From 3cdec996ac7d8be189a4adbe9019d66462897686 Mon Sep 17 00:00:00 2001 From: Isaac To Date: Fri, 23 Jan 2026 10:50:38 -0800 Subject: [PATCH 1/3] feat: replace support of "datetime_filesafe" format field in output file prefix format string with "datetime" --- README.md | 11 +++++------ src/con_duct/_duct_main.py | 2 +- src/con_duct/_models.py | 7 ++++++- src/con_duct/cli.py | 4 ++-- test/duct_main/test_log_paths.py | 4 ++-- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1da438e0..5ee28d72 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ environment variables: DUCT_REPORT_INTERVAL=120.0 # Set default output location - DUCT_OUTPUT_PREFIX=~/duct-logs/{datetime_filesafe}-{pid}_ + DUCT_OUTPUT_PREFIX=~/duct-logs/{datetime}-{pid}_ # Add execution notes (multiline) DUCT_MESSAGE="Experiment run for paper revision @@ -173,11 +173,10 @@ options: File string format to be used as a prefix for the files -- the captured stdout and stderr and the resource usage logs. The understood variables are - {datetime}, {datetime_filesafe}, and {pid}. Leading - directories will be created if they do not exist. You - can also provide value via DUCT_OUTPUT_PREFIX env - variable. (default: - .duct/logs/{datetime_filesafe}-{pid}_) + {datetime} and {pid}. Leading directories will be + created if they do not exist. You can also provide + value via DUCT_OUTPUT_PREFIX env variable. (default: + .duct/logs/{datetime}-{pid}_) --summary-format SUMMARY_FORMAT Output template to use when printing the summary following execution. Accepts custom conversion flags: diff --git a/src/con_duct/_duct_main.py b/src/con_duct/_duct_main.py index 8c2623ee..ca9728b9 100644 --- a/src/con_duct/_duct_main.py +++ b/src/con_duct/_duct_main.py @@ -17,7 +17,7 @@ lgr = logging.getLogger("con-duct") DUCT_OUTPUT_PREFIX = os.getenv( - "DUCT_OUTPUT_PREFIX", ".duct/logs/{datetime_filesafe}-{pid}_" + "DUCT_OUTPUT_PREFIX", ".duct/logs/{datetime}-{pid}_" ) EXECUTION_SUMMARY_FORMAT = ( "Summary:\n" diff --git a/src/con_duct/_models.py b/src/con_duct/_models.py index 89c45bef..d3400893 100644 --- a/src/con_duct/_models.py +++ b/src/con_duct/_models.py @@ -131,7 +131,12 @@ def __iter__(self) -> Iterator[tuple[str, str]]: def create(cls, output_prefix: str, pid: None | int = None) -> LogPaths: datetime_filesafe = datetime.now().strftime("%Y.%m.%dT%H.%M.%S") formatted_prefix = output_prefix.format( - pid=pid, datetime_filesafe=datetime_filesafe + pid=pid, + datetime=datetime_filesafe, + # Use of the `datetime_filesafe` format field is deprecated. + # The setting of it here is to provide for backwards compatibility + # It should be removed eventually + datetime_filesafe=datetime_filesafe, ) return cls( stdout=f"{formatted_prefix}{SUFFIXES['stdout']}", diff --git a/src/con_duct/cli.py b/src/con_duct/cli.py index 2de8b42d..258d00aa 100644 --- a/src/con_duct/cli.py +++ b/src/con_duct/cli.py @@ -163,7 +163,7 @@ def _replay_early_logs(log_buffer: List[tuple[str, str]]) -> None: DUCT_REPORT_INTERVAL=120.0 # Set default output location - DUCT_OUTPUT_PREFIX=~/duct-logs/{{datetime_filesafe}}-{{pid}}_ + DUCT_OUTPUT_PREFIX=~/duct-logs/{{datetime}}-{{pid}}_ # Add execution notes (multiline) DUCT_MESSAGE="Experiment run for paper revision @@ -267,7 +267,7 @@ def _create_run_parser() -> argparse.ArgumentParser: default=DUCT_OUTPUT_PREFIX, help="File string format to be used as a prefix for the files -- the captured " "stdout and stderr and the resource usage logs. The understood variables are " - "{datetime}, {datetime_filesafe}, and {pid}. " + "{datetime} and {pid}. " "Leading directories will be created if they do not exist. " "You can also provide value via DUCT_OUTPUT_PREFIX env variable. ", ) diff --git a/test/duct_main/test_log_paths.py b/test/duct_main/test_log_paths.py index 86113756..9568b181 100644 --- a/test/duct_main/test_log_paths.py +++ b/test/duct_main/test_log_paths.py @@ -7,8 +7,8 @@ from con_duct._models import LogPaths, Outputs -def test_log_paths_filesafe_datetime_prefix() -> None: - log_paths = LogPaths.create("start_{datetime_filesafe}") +def test_log_paths_datetime_prefix() -> None: + log_paths = LogPaths.create("start_{datetime}") pattern = r"^start_\d{4}\.\d{2}\.\d{2}T\d{2}\.\d{2}\.\d{2}.*" for path in asdict(log_paths).values(): assert re.match(pattern, path) is not None From 131b042115462e73db9a027604956179e7a89bfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:57:50 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/con_duct/_duct_main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/con_duct/_duct_main.py b/src/con_duct/_duct_main.py index ca9728b9..f83f7db1 100644 --- a/src/con_duct/_duct_main.py +++ b/src/con_duct/_duct_main.py @@ -16,9 +16,7 @@ lgr = logging.getLogger("con-duct") -DUCT_OUTPUT_PREFIX = os.getenv( - "DUCT_OUTPUT_PREFIX", ".duct/logs/{datetime}-{pid}_" -) +DUCT_OUTPUT_PREFIX = os.getenv("DUCT_OUTPUT_PREFIX", ".duct/logs/{datetime}-{pid}_") EXECUTION_SUMMARY_FORMAT = ( "Summary:\n" "Exit Code: {exit_code!E}\n" From 1d2ac008694a9e86a8e52ae269a043578c814dd4 Mon Sep 17 00:00:00 2001 From: Austin Macdonald Date: Mon, 2 Feb 2026 11:54:22 -0600 Subject: [PATCH 3/3] test: add test for deprecated {datetime_filesafe} format field Co-Authored-By: Claude Opus 4.5 --- test/duct_main/test_log_paths.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/duct_main/test_log_paths.py b/test/duct_main/test_log_paths.py index 9568b181..389904fe 100644 --- a/test/duct_main/test_log_paths.py +++ b/test/duct_main/test_log_paths.py @@ -14,6 +14,14 @@ def test_log_paths_datetime_prefix() -> None: assert re.match(pattern, path) is not None +def test_log_paths_deprecated_datetime_filesafe_prefix() -> None: + """Ensure deprecated {datetime_filesafe} format field still works.""" + log_paths = LogPaths.create("start_{datetime_filesafe}") + pattern = r"^start_\d{4}\.\d{2}\.\d{2}T\d{2}\.\d{2}\.\d{2}.*" + for path in asdict(log_paths).values(): + assert re.match(pattern, path) is not None + + def test_log_paths_pid_prefix() -> None: prefix = "prefix_{pid}_" log_paths = LogPaths.create(prefix, pid=123456)