From beec7bac494db6b6d023eece651fdcfa80090968 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:53:04 +0800 Subject: [PATCH] Stop format_name_template from mutating the caller's substitutions `substitutions or {}` only builds a new dict when the argument is falsy, so a non-empty dict is aliased and then has date/time written into it: subs = {"custom": "abc"} format_name_template("run-{custom}-{date}-{time}", substitutions=subs) subs # {'custom': 'abc', 'date': '2026_07_16', 'time': '16_50_23_522789'} An empty dict masks this (`{} or {}` yields a new dict), which is likely why it went unnoticed. _create_snapshot passes pipeline_configuration.substitutions straight in when deriving a schedule name, before the snapshot is serialized into the PipelineSnapshotRequest, so the injected date/time get persisted. Because finalize_substitutions uses setdefault, the baked-in values then win over the real start time forever after: cfg.finalize_substitutions(start_time=datetime(2027, 1, 1))["date"] # '2026_07_16' -- the future start time is ignored Copy the dict instead. Explicit date/time substitutions still take precedence, and the returned name is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/zenml/utils/string_utils.py | 2 +- tests/unit/utils/test_string_utils.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/zenml/utils/string_utils.py b/src/zenml/utils/string_utils.py index 3cee230e1e2..f0d8519bc92 100644 --- a/src/zenml/utils/string_utils.py +++ b/src/zenml/utils/string_utils.py @@ -183,7 +183,7 @@ def format_name_template( KeyError: If a key in template is missing in the substitutions. ValueError: If the formatted name is empty. """ - substitutions = substitutions or {} + substitutions = dict(substitutions or {}) if ("date" not in substitutions and "{date}" in name_template) or ( "time" not in substitutions and "{time}" in name_template diff --git a/tests/unit/utils/test_string_utils.py b/tests/unit/utils/test_string_utils.py index c2d715e63ac..861862f5555 100644 --- a/tests/unit/utils/test_string_utils.py +++ b/tests/unit/utils/test_string_utils.py @@ -116,3 +116,29 @@ def substitution_func(s): {3: "value_suffix", "key_suffix": model_sub}, set(["set_value_suffix", 4]), ] + + +def test_format_name_template_does_not_mutate_substitutions() -> None: + """The caller's substitutions dict must not be modified. + + `substitutions or {}` only builds a new dict for a falsy argument, so a + non-empty dict was aliased and had date/time injected into it. + """ + substitutions = {"custom": "value"} + + formatted = string_utils.format_name_template( + "run-{custom}-{date}-{time}", substitutions=substitutions + ) + + assert substitutions == {"custom": "value"} + assert formatted.startswith("run-value-") + + +def test_format_name_template_uses_given_substitutions() -> None: + """Explicit date/time substitutions are still honored.""" + formatted = string_utils.format_name_template( + "run-{date}-{time}", + substitutions={"date": "2020_01_01", "time": "00_00_00"}, + ) + + assert formatted == "run-2020_01_01-00_00_00"