diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 520379e51d..59d851c02e 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -599,9 +599,14 @@ def _load_yaml_internal(fname: Path) -> Any: listener(fname) try: with fname.open(encoding="utf-8") as f_handle: - return parse_yaml(fname, f_handle) + res = parse_yaml(fname, f_handle) except (UnicodeDecodeError, OSError) as err: raise EsphomeError(f"Error reading file {fname}: {err}") from err + # Top-level !include returns a deferred IncludeFile; resolve it so + # callers always receive the final content. + if isinstance(res, IncludeFile): + res = res.load() + return res def parse_yaml(file_name: Path, file_handle: TextIOWrapper, yaml_loader=None) -> Any: diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 2c01019abd..bfd60de44d 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -640,6 +640,18 @@ def test_include_in_list_context() -> None: assert config["values"] == ["alpha", "beta", "gamma"] +def test_top_level_include_resolved_by_load_yaml(tmp_path: Path) -> None: + """load_yaml resolves a top-level !include so callers always get a dict.""" + child = tmp_path / "child.yaml" + child.write_text("key: value\n") + main = tmp_path / "main.yaml" + main.write_text("!include child.yaml\n") + + result = yaml_util.load_yaml(main) + assert isinstance(result, dict) + assert result["key"] == "value" + + def test_include_plain_filename_loads_after_deferred_refactor() -> None: """!include with a plain filename (no $ expressions) still loads correctly.