diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index a46783bb58..252a24061a 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -10,6 +10,7 @@ from esphome.components.substitutions import ( ContextVars, push_context, resolve_include, + resolve_substitutions_block, substitute, ) from esphome.components.substitutions.jinja import has_jinja @@ -516,23 +517,12 @@ def do_packages_pass( if CONF_PACKAGES not in config: return config - raw_substitutions = config.pop(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(command_line_substitutions or {}), - strict_undefined=False, + substitutions = UserDict( + resolve_substitutions_block( + config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions ) - 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( substitutions, command_line_substitutions, skip_update ) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 6f24011210..dfdf186e5a 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -414,6 +414,31 @@ def _warn_unresolved_variables(errors: ErrList) -> None: ) +def resolve_substitutions_block( + substitutions: Any, + command_line_substitutions: dict[str, Any] | None, +) -> dict[str, Any]: + """Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape. + + The caller is responsible for wrapping the call in + ``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting. + ``command_line_substitutions`` seeds the filename context so + ``substitutions: !include ${var}.yaml`` can reference CLI-provided vars. + """ + if isinstance(substitutions, IncludeFile): + substitutions, _ = resolve_include( + substitutions, + [], + ContextVars(command_line_substitutions or {}), + strict_undefined=False, + ) + if not isinstance(substitutions, dict): + raise cv.Invalid( + f"Substitutions must be a key to value mapping, got {type(substitutions)}" + ) + return substitutions + + def do_substitution_pass( config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None ) -> OrderedDict: @@ -429,20 +454,9 @@ def do_substitution_pass( # Use merge_dicts_ordered to preserve OrderedDict type for move_to_end() substitutions = config.pop(CONF_SUBSTITUTIONS, {}) 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(command_line_substitutions or {}), - strict_undefined=False, - ) - if not isinstance(substitutions, dict): - raise cv.Invalid( - f"Substitutions must be a key to value mapping, got {type(substitutions)}" - ) + substitutions = resolve_substitutions_block( + substitutions, command_line_substitutions + ) substitutions = merge_dicts_ordered( substitutions, command_line_substitutions or {} )