diff --git a/esphome/final_validate.py b/esphome/final_validate.py index cebd2f1cdad..46be8f0b3f5 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -3,7 +3,7 @@ import contextvars from typing import Any import esphome.config_validation as cv -from esphome.types import ID, ConfigFragmentType, ConfigPathType +from esphome.types import ID, ConfigFragmentType, ConfigPathType, ConfigType class FinalValidateConfig(ABC): @@ -39,6 +39,21 @@ full_config: contextvars.ContextVar[FinalValidateConfig] = contextvars.ContextVa ) +def get_declaring_config( + id_: ID, config: FinalValidateConfig | None = None +) -> ConfigType: + """Return the config block that declares id_, such as a hub's own config. + + Uses the full config being validated unless config is given. + Raises KeyError if the id was not declared, TypeError if its parent is not a dict. + """ + fconf = full_config.get() if config is None else config + declaring = fconf.get_config_for_path(fconf.get_path_for_id(id_)[:-1]) + if not isinstance(declaring, dict): + raise TypeError(f"ID {id_} is not declared in a config block") + return declaring + + def id_declaration_match_schema(schema): """A final-validation schema function that applies a schema to the outer config fragment of an ID declaration. diff --git a/tests/unit_tests/test_final_validate.py b/tests/unit_tests/test_final_validate.py new file mode 100644 index 00000000000..834dc92ffbd --- /dev/null +++ b/tests/unit_tests/test_final_validate.py @@ -0,0 +1,47 @@ +"""Tests for esphome.final_validate helpers.""" + +import pytest + +from esphome import final_validate as fv +from esphome.config import Config +from esphome.const import CONF_ID +from esphome.core import ID + + +def _config() -> tuple[Config, ID, dict]: + hub_id = ID("my_hub", is_declaration=True) + hub = {CONF_ID: hub_id, "mode": "fast"} + config = Config() + config["other"] = {"x": 1} + config["my_comp"] = [{"y": 2}, hub] + config.declare_ids.append((hub_id, ["my_comp", 1, CONF_ID])) + return config, hub_id, hub + + +def test_returns_the_declaring_block_from_the_full_config() -> None: + config, hub_id, hub = _config() + token = fv.full_config.set(config) + try: + assert fv.get_declaring_config(hub_id) is hub + finally: + fv.full_config.reset(token) + + +def test_uses_the_given_config() -> None: + config, hub_id, hub = _config() + assert fv.get_declaring_config(ID("my_hub"), config) is hub + + +def test_unknown_id_raises_key_error() -> None: + config, _, _ = _config() + with pytest.raises(KeyError): + fv.get_declaring_config(ID("missing"), config) + + +def test_non_dict_parent_raises_type_error() -> None: + config, _, _ = _config() + list_id = ID("in_list", is_declaration=True) + config["listed"] = [list_id] + config.declare_ids.append((list_id, ["listed", 0])) + with pytest.raises(TypeError): + fv.get_declaring_config(ID("in_list"), config)