[packages] Add resolve_packages single-call seam (#16235)

This commit is contained in:
J. Nick Koston
2026-05-05 18:26:52 -05:00
committed by GitHub
parent 39b2b901f7
commit 67491c3194
2 changed files with 179 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ from esphome.components.packages import (
do_packages_pass,
is_package_definition,
merge_packages,
resolve_packages,
)
from esphome.components.substitutions import ContextVars, do_substitution_pass
import esphome.config as config_module
@@ -1621,3 +1622,122 @@ def test_remote_package_vars_resolved_against_sibling_package_substitutions(
actual = packages_pass(config)
assert actual[CONF_SENSOR][0]["pin"] == "GPIO5"
# ---------------------------------------------------------------------------
# resolve_packages — single-call wrapper around do_packages_pass + merge_packages
# ---------------------------------------------------------------------------
def test_resolve_packages_returns_config_unchanged_without_packages() -> None:
"""No ``packages:`` key → no-op, same dict back."""
config = {CONF_ESPHOME: {CONF_NAME: "test"}, CONF_WIFI: {CONF_SSID: "x"}}
result = resolve_packages(config)
assert result is config
assert CONF_PACKAGES not in result
def test_resolve_packages_loads_and_merges_in_one_call() -> None:
"""End-to-end: a config with one local-dict package gets its blocks flattened."""
config = {
CONF_ESPHOME: {CONF_NAME: "main"},
CONF_PACKAGES: {
"shared": {
CONF_WIFI: {CONF_SSID: "from_package"},
CONF_SENSOR: [
{CONF_PLATFORM: "template", CONF_NAME: "from_package_sensor"},
],
}
},
}
result = resolve_packages(config)
# ``packages:`` is gone — it was consumed by the merge.
assert CONF_PACKAGES not in result
# Blocks contributed by the package are now top-level.
assert result[CONF_WIFI][CONF_SSID] == "from_package"
assert result[CONF_SENSOR][0][CONF_NAME] == "from_package_sensor"
# The main config's own keys survive untouched.
assert result[CONF_ESPHOME][CONF_NAME] == "main"
def test_resolve_packages_preserves_main_config_overrides() -> None:
"""Main-config values win over package values for the same key.
Pinning the precedence ESPHome's compiler uses so any future
refactor of the wrapper doesn't accidentally flip the order.
"""
config = {
CONF_ESPHOME: {CONF_NAME: "main"},
CONF_WIFI: {CONF_SSID: "main_wins"},
CONF_PACKAGES: {
"shared": {CONF_WIFI: {CONF_SSID: "package_loses"}},
},
}
result = resolve_packages(config)
assert result[CONF_WIFI][CONF_SSID] == "main_wins"
def test_resolve_packages_forwards_command_line_substitutions() -> None:
"""``command_line_substitutions`` reaches the underlying ``do_packages_pass``.
The wrapper exists so external tools have one stable seam; if
that seam silently dropped a kwarg the underlying call accepts,
callers would see surprising behaviour. This pins the
pass-through.
"""
config = {
CONF_ESPHOME: {CONF_NAME: "main"},
CONF_PACKAGES: {"shared": {CONF_WIFI: {CONF_SSID: "from_package"}}},
}
with patch(
"esphome.components.packages.do_packages_pass",
wraps=do_packages_pass,
) as spy:
resolve_packages(config, command_line_substitutions={"foo": "bar"})
spy.assert_called_once()
_, kwargs = spy.call_args
assert kwargs.get("command_line_substitutions") == {"foo": "bar"}
def test_resolve_packages_does_not_run_substitutions() -> None:
"""``${var}`` placeholders inside package content stay literal.
The full ``validate_config`` pipeline runs ``do_substitution_pass``
BETWEEN ``do_packages_pass`` and ``merge_packages``; this wrapper
skips it on purpose. Pin that contract so a future refactor can't
silently start resolving substitutions and break callers that
deliberately compose the passes themselves.
"""
config = {
CONF_ESPHOME: {CONF_NAME: "main"},
CONF_SUBSTITUTIONS: {"ssid_value": "resolved_ssid"},
CONF_PACKAGES: {
"shared": {CONF_WIFI: {CONF_SSID: "${ssid_value}"}},
},
}
result = resolve_packages(config)
# Without ``do_substitution_pass`` the placeholder is preserved.
assert result[CONF_WIFI][CONF_SSID] == "${ssid_value}"
def test_resolve_packages_does_not_apply_extend_remove() -> None:
"""Top-level ``!remove`` / ``!extend`` markers stay in the merged dict.
The full ``validate_config`` pipeline runs ``resolve_extend_remove``
AFTER ``merge_packages``; this wrapper skips it on purpose. Pin
that contract: a package-contributed block paired with a top-level
``!remove`` is left as-is for callers to handle (or for them to
call ``resolve_extend_remove`` themselves).
"""
config = {
CONF_ESPHOME: {CONF_NAME: "main"},
CONF_WIFI: Remove(),
CONF_PACKAGES: {
"shared": {CONF_WIFI: {CONF_SSID: "from_package"}},
},
}
result = resolve_packages(config)
# ``merge_packages`` keeps the top-level ``!remove`` (it wins
# over the package value during merge), and the marker is not
# resolved by this wrapper.
assert isinstance(result[CONF_WIFI], Remove)