From e67b65b669b8a92d0d10edb38448807dfb13d50d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Apr 2026 02:42:03 -0500 Subject: [PATCH] [substitutions] [packages] Address review feedback - Seed `resolve_include` context with `command_line_substitutions` so `substitutions: !include ${var}.yaml` can reference CLI-provided vars in the include filename (parallels the `packages: !include` path). - Validate shape of resolved substitutions in `do_packages_pass` and raise `cv.Invalid` under `CONF_SUBSTITUTIONS` instead of letting `UserDict()` fail with a low-level exception on a non-mapping. - Fixture 17 exercises the CLI-templated include filename. --- esphome/components/packages/__init__.py | 17 +++++++++++++---- esphome/components/substitutions/__init__.py | 7 ++++++- ...-substitutions_include_cli_var.approved.yaml | 6 ++++++ .../17-substitutions_include_cli_var.input.yaml | 8 ++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 58acc94a41..a46783bb58 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -517,11 +517,20 @@ def do_packages_pass( return config raw_substitutions = config.pop(CONF_SUBSTITUTIONS, {}) - if isinstance(raw_substitutions, yaml_util.IncludeFile): - # Resolve `substitutions: !include file.yaml` before feeding into UserDict. - with cv.prepend_path(CONF_SUBSTITUTIONS): + with cv.prepend_path(CONF_SUBSTITUTIONS): + if isinstance(raw_substitutions, yaml_util.IncludeFile): + # Resolve `substitutions: !include file.yaml` before feeding into UserDict. + # Seed with command-line substitutions so `!include ${var}.yaml` can + # reference CLI-provided vars in the filename. raw_substitutions, _ = resolve_include( - raw_substitutions, [], ContextVars(), strict_undefined=False + raw_substitutions, + [], + ContextVars(command_line_substitutions or {}), + strict_undefined=False, + ) + if not isinstance(raw_substitutions, dict): + raise cv.Invalid( + f"Substitutions must be a key to value mapping, got {type(raw_substitutions)}" ) substitutions = UserDict(raw_substitutions) processor = _PackageProcessor( diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 3cdf79beea..6f24011210 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -431,8 +431,13 @@ def do_substitution_pass( with cv.prepend_path(CONF_SUBSTITUTIONS): if isinstance(substitutions, IncludeFile): # Resolve `substitutions: !include file.yaml` before validating the shape. + # Seed with command-line substitutions so `!include ${var}.yaml` can + # reference CLI-provided vars in the filename. substitutions, _ = resolve_include( - substitutions, [], ContextVars(), strict_undefined=False + substitutions, + [], + ContextVars(command_line_substitutions or {}), + strict_undefined=False, ) if not isinstance(substitutions, dict): raise cv.Invalid( diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml new file mode 100644 index 0000000000..f1fd5fb078 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.approved.yaml @@ -0,0 +1,6 @@ +substitutions: + subs_file: 15-substitutions_inc + wifi_password: sub_password +wifi: + ssid: main_ssid + password: sub_password diff --git a/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml new file mode 100644 index 0000000000..3248504b46 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/17-substitutions_include_cli_var.input.yaml @@ -0,0 +1,8 @@ +command_line_substitutions: + subs_file: 15-substitutions_inc + +substitutions: !include ${subs_file}.yaml + +wifi: + ssid: main_ssid + password: $wifi_password