From 2fe169a7ae78edb218b35a33e7ed133ebe26a62e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Sep 2026 11:59:54 +0200 Subject: [PATCH] Use relative paths in configuration error messages (#182427) --- homeassistant/config.py | 8 +- .../component_exceptions/configuration.yaml | 5 ++ .../error_processing.yaml | 6 ++ .../component_exceptions/multi_platform.yaml | 3 + .../config/component_exceptions/platform.yaml | 2 + tests/snapshots/test_config.ambr | 6 +- tests/test_config.py | 73 ++++++++++--------- 7 files changed, 62 insertions(+), 41 deletions(-) create mode 100644 tests/fixtures/core/config/component_exceptions/configuration.yaml create mode 100644 tests/fixtures/core/config/component_exceptions/error_processing.yaml create mode 100644 tests/fixtures/core/config/component_exceptions/multi_platform.yaml create mode 100644 tests/fixtures/core/config/component_exceptions/platform.yaml diff --git a/homeassistant/config.py b/homeassistant/config.py index c2c7ed5e3362..ca29e418c512 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -826,8 +826,8 @@ def _get_log_message_and_stack_print_pref( hass, exception, platform_path, platform_config, link ) if annotation := find_annotation(platform_config, exception.path): - placeholders["config_file"], line = annotation - placeholders["line"] = str(line) + placeholders["config_file"] = _relpath(hass, annotation[0]) + placeholders["line"] = str(annotation[1]) else: if TYPE_CHECKING: assert isinstance(exception, HomeAssistantError) @@ -835,8 +835,8 @@ def _get_log_message_and_stack_print_pref( hass, exception, platform_path, platform_config, link ) if annotation := find_annotation(platform_config, [platform_path]): - placeholders["config_file"], line = annotation - placeholders["line"] = str(line) + placeholders["config_file"] = _relpath(hass, annotation[0]) + placeholders["line"] = str(annotation[1]) show_stack_trace = True return (log_message, show_stack_trace, placeholders) diff --git a/tests/fixtures/core/config/component_exceptions/configuration.yaml b/tests/fixtures/core/config/component_exceptions/configuration.yaml new file mode 100644 index 000000000000..64d329ae8648 --- /dev/null +++ b/tests/fixtures/core/config/component_exceptions/configuration.yaml @@ -0,0 +1,5 @@ +# 'other_domain' keeps the root mapping's line different from the 'test_domain' +# key's line, so an asserted line number says which of the two find_annotation +# could answer with actually answered. +other_domain: {} +test_domain: {} diff --git a/tests/fixtures/core/config/component_exceptions/error_processing.yaml b/tests/fixtures/core/config/component_exceptions/error_processing.yaml new file mode 100644 index 000000000000..3f0f4e1bc4ba --- /dev/null +++ b/tests/fixtures/core/config/component_exceptions/error_processing.yaml @@ -0,0 +1,6 @@ +# 'other_domain' keeps the root mapping's line different from the 'test_domain' +# key's line: an error reported at 'test_domain' gets the key's line, while an +# error reported at a path absent from the config falls back to the root +# container's line. +other_domain: {} +test_domain: [] diff --git a/tests/fixtures/core/config/component_exceptions/multi_platform.yaml b/tests/fixtures/core/config/component_exceptions/multi_platform.yaml new file mode 100644 index 000000000000..2d47a96a2567 --- /dev/null +++ b/tests/fixtures/core/config/component_exceptions/multi_platform.yaml @@ -0,0 +1,3 @@ +test_domain: + - platform: test_platform1 + - platform: test_platform2 diff --git a/tests/fixtures/core/config/component_exceptions/platform.yaml b/tests/fixtures/core/config/component_exceptions/platform.yaml new file mode 100644 index 000000000000..3141a9247336 --- /dev/null +++ b/tests/fixtures/core/config/component_exceptions/platform.yaml @@ -0,0 +1,2 @@ +test_domain: + platform: test_platform diff --git a/tests/snapshots/test_config.ambr b/tests/snapshots/test_config.ambr index deafe501c74f..161bd176063b 100644 --- a/tests/snapshots/test_config.ambr +++ b/tests/snapshots/test_config.ambr @@ -3,13 +3,13 @@ 'Unable to import test_domain: bla' # --- # name: test_component_config_error_processing[exception_info_list1-bla-messages1-True-config_validation_err] - 'Invalid config for integration test_domain at configuration.yaml, line 140: bla' + 'Invalid config for integration test_domain at error_processing.yaml, line 6: bla' # --- # name: test_component_config_error_processing[exception_info_list2-bla at 'path'-messages2-False-config_validation_err] - "Invalid config for integration test_domain at configuration.yaml, line 140: bla at 'path'" + "Invalid config for integration test_domain at error_processing.yaml, line 5: bla at 'path'" # --- # name: test_component_config_error_processing[exception_info_list3-bla at 'path'-messages3-False-platform_config_validation_err] - "Invalid config for test_domain from integration test_domain at file configuration.yaml, line 140: bla at 'path'. Check the logs for more information" + "Invalid config for test_domain from integration test_domain at file error_processing.yaml, line 5: bla at 'path'. Check the logs for more information" # --- # name: test_component_config_error_processing[exception_info_list4-bla-messages4-False-platform_component_load_err] 'Platform error: test_domain - bla' diff --git a/tests/test_config.py b/tests/test_config.py index 6c4d2332998d..27d28405989c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -22,8 +22,7 @@ from homeassistant.exceptions import ConfigValidationError, HomeAssistantError from homeassistant.helpers import check_config, config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration -from homeassistant.util.yaml import SECRET_YAML -from homeassistant.util.yaml.objects import NodeDictClass +from homeassistant.util.yaml import SECRET_YAML, load_yaml_dict from .common import ( MockModule, @@ -41,6 +40,13 @@ AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) SCENES_PATH = os.path.join(CONFIG_DIR, config_util.SCENE_CONFIG_PATH) SAFE_MODE_PATH = os.path.join(CONFIG_DIR, config_util.SAFE_MODE_FILENAME) +COMPONENT_EXCEPTIONS_DIR = os.path.join( + os.path.dirname(__file__), "fixtures", "core", "config", "component_exceptions" +) +COMPONENT_EXCEPTIONS_YAML = os.path.join(COMPONENT_EXCEPTIONS_DIR, "configuration.yaml") +ERROR_PROCESSING_CONFIG = load_yaml_dict( + os.path.join(COMPONENT_EXCEPTIONS_DIR, "error_processing.yaml") +) def create_file(path): @@ -349,13 +355,6 @@ async def mock_custom_validator_integrations_with_docs( ) -class ConfigTestClass(NodeDictClass): - """Test class for config with wrapper.""" - - __line__ = 140 - __config_file__ = "configuration.yaml" - - async def test_create_default_config(hass: HomeAssistant) -> None: """Test creation of default config.""" assert not os.path.isfile(YAML_PATH) @@ -839,20 +838,19 @@ async def test_merge_split_component_definition(hass: HomeAssistant) -> None: async def test_component_config_exceptions( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test unexpected exceptions validating component config.""" + """Test unexpected exceptions validating component config. - # Create test config with embedded info - test_config = ConfigTestClass({"test_domain": {}}) - test_platform_config = ConfigTestClass( - {"test_domain": {"platform": "test_platform"}} + The fixture puts the root mapping on line 4 and the 'test_domain' key on line 5, + so asserting line 5 pins that find_annotation answered from the key. + """ + + hass.config.config_dir = COMPONENT_EXCEPTIONS_DIR + test_config = load_yaml_dict(COMPONENT_EXCEPTIONS_YAML) + test_platform_config = load_yaml_dict( + os.path.join(COMPONENT_EXCEPTIONS_DIR, "platform.yaml") ) - test_multi_platform_config = ConfigTestClass( - { - "test_domain": [ - {"platform": "test_platform1"}, - {"platform": "test_platform2"}, - ] - }, + test_multi_platform_config = load_yaml_dict( + os.path.join(COMPONENT_EXCEPTIONS_DIR, "multi_platform.yaml") ) test_integration = Mock( @@ -901,8 +899,8 @@ async def test_component_config_exceptions( is None ) assert ( - "Invalid config for 'test_domain' at ../../configuration.yaml, " - "line 140: broken, please check the docs at" in caplog.text + "Invalid config for 'test_domain' at configuration.yaml, " + "line 5: broken, please check the docs at" in caplog.text ) with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( @@ -911,7 +909,7 @@ async def test_component_config_exceptions( assert ( str(ex.value) == "Invalid config for integration test_domain at configuration.yaml, " - "line 140: broken" + "line 5: broken" ) # component.CONFIG_SCHEMA caplog.clear() @@ -1191,7 +1189,7 @@ async def test_component_config_exceptions( ImportError("bla"), "component_import_err", "test_domain", - ConfigTestClass({"test_domain": []}), + ERROR_PROCESSING_CONFIG, "https://example.com", ) ], @@ -1206,14 +1204,14 @@ async def test_component_config_exceptions( HomeAssistantError("bla"), "config_validation_err", "test_domain", - ConfigTestClass({"test_domain": []}), + ERROR_PROCESSING_CONFIG, "https://example.com", ) ], "bla", [ "Invalid config for 'test_domain' at " - "../../configuration.yaml, line 140: bla, " + "error_processing.yaml, line 6: bla, " "please check the docs at https://example.com", "bla", ], @@ -1226,14 +1224,14 @@ async def test_component_config_exceptions( probatio.Invalid("bla", ["path"]), "config_validation_err", "test_domain", - ConfigTestClass({"test_domain": []}), + ERROR_PROCESSING_CONFIG, "https://example.com", ) ], "bla at 'path'", [ "Invalid config for 'test_domain' at " - "../../configuration.yaml, line 140: bla 'path', " + "error_processing.yaml, line 5: bla 'path', " "got None, please check the docs at https://example.com", "bla", ], @@ -1246,14 +1244,14 @@ async def test_component_config_exceptions( probatio.Invalid("bla", ["path"]), "platform_config_validation_err", "test_domain", - ConfigTestClass({"test_domain": []}), + ERROR_PROCESSING_CONFIG, "https://alt.example.com", ) ], "bla at 'path'", [ "Invalid config for 'test_domain' at " - "../../configuration.yaml, line 140: bla 'path', " + "error_processing.yaml, line 5: bla 'path', " "got None, please check the docs at https://alt.example.com", "bla", ], @@ -1266,7 +1264,7 @@ async def test_component_config_exceptions( ImportError("bla"), "platform_component_load_err", "test_domain", - ConfigTestClass({"test_domain": []}), + ERROR_PROCESSING_CONFIG, "https://example.com", ) ], @@ -1287,8 +1285,15 @@ async def test_component_config_error_processing( show_stack_trace: bool, translation_key: str, ) -> None: - """Test component config error processing.""" + """Test component config error processing. + The fixture puts the root mapping on line 5 and the 'test_domain' key on line 6. + An expected message naming line 6 therefore pins that find_annotation answered + from the key, and one naming line 5 that it fell back to the container, which is + what happens when the reported path is absent from the config. + """ + + hass.config.config_dir = COMPONENT_EXCEPTIONS_DIR test_integration = Mock( domain="test_domain", documentation="https://example.com", @@ -1323,7 +1328,7 @@ async def test_component_config_error_processing( return_value=config_util.IntegrationConfigInfo(None, exception_info_list), ): await config_util.async_process_component_and_handle_errors( - hass, ConfigTestClass({}), test_integration + hass, {}, test_integration ) assert all(message in caplog.text for message in messages)