diff --git a/homeassistant/config.py b/homeassistant/config.py index 17a6f32336f..b35ba910ac3 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from awesomeversion import AwesomeVersion import voluptuous as vol -from voluptuous.humanize import humanize_error +from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH from . import auth from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers @@ -578,6 +578,47 @@ def find_annotation( return find_annotation_rec(config, list(path), None) +def stringify_invalid(ex: vol.Invalid) -> str: + """Stringify voluptuous.Invalid. + + Based on voluptuous.error.Invalid.__str__, the main modification + is to format the path delimited by -> instead of @data[]. + """ + path = "->".join(str(m) for m in ex.path) + # This function is an alternative to the stringification done by + # vol.Invalid.__str__, so we need to call Exception.__str__ here + # instead of str(ex) + output = Exception.__str__(ex) + if error_type := ex.error_type: + output += " for " + error_type + return f"{output} '{path}'" + + +def humanize_error( + data: Any, + validation_error: vol.Invalid, + max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH, +) -> str: + """Provide a more helpful + complete validation error message. + + This is a modified version of voluptuous.error.Invalid.__str__, + the modifications make some minor changes to the formatting. + """ + if isinstance(validation_error, vol.MultipleInvalid): + return "\n".join( + sorted( + humanize_error(data, sub_error, max_sub_error_length) + for sub_error in validation_error.errors + ) + ) + offending_item_summary = repr(_get_by_path(data, validation_error.path)) + if len(offending_item_summary) > max_sub_error_length: + offending_item_summary = ( + f"{offending_item_summary[: max_sub_error_length - 3]}..." + ) + return f"{stringify_invalid(validation_error)}, got {offending_item_summary}" + + @callback def _format_config_error( ex: Exception, domain: str, config: dict, link: str | None = None diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 9210b9ea738..d04757fb808 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -204,7 +204,7 @@ async def test_optimistic_states(hass: HomeAssistant, start_ha) -> None: { "alarm_control_panel": {"platform": "template"}, }, - "required key not provided @ data['panels']", + "required key not provided 'panels'", ), ( { diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index baec4ae04a9..11f65fa4e5e 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -83,8 +83,7 @@ async def test_bad_core_config(hass: HomeAssistant) -> None: ( "Invalid config for [homeassistant] at " f"{hass.config.path(YAML_CONFIG_FILE)}, line 2: " - "not a valid value for dictionary value @ data['unit_system']. Got " - "'bad'." + "not a valid value for dictionary value 'unit_system', got 'bad'." ), "homeassistant", {"unit_system": "bad"}, diff --git a/tests/snapshots/test_config.ambr b/tests/snapshots/test_config.ambr index fbf37e69ead..33b5e193d87 100644 --- a/tests/snapshots/test_config.ambr +++ b/tests/snapshots/test_config.ambr @@ -1,54 +1,54 @@ # serializer version: 1 # name: test_component_config_validation_error[basic] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 6: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 9: expected str for dictionary value @ data['option1']. Got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 6: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 9: expected str for dictionary value 'option1', got 123.", "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 12: 'no_such_option' is an invalid option for [iot_domain.non_adr_0007], check: no_such_option", - "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 19: required key not provided @ data['adr_0007_2']['host']. Got None.", - "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 24: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.", + "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 19: required key not provided 'adr_0007_2->host', got None.", + "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 24: expected int for dictionary value 'adr_0007_3->port', got 'foo'.", "Invalid config for [adr_0007_4] at /fixtures/core/config/component_validation/basic/configuration.yaml, line 29: 'no_such_option' is an invalid option for [adr_0007_4], check: adr_0007_4->no_such_option", ]) # --- # name: test_component_config_validation_error[basic_include] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 5: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 8: expected str for dictionary value @ data['option1']. Got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 5: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 8: expected str for dictionary value 'option1', got 123.", "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 11: 'no_such_option' is an invalid option for [iot_domain.non_adr_0007], check: no_such_option", - "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/basic_include/configuration.yaml, line 3: required key not provided @ data['adr_0007_2']['host']. Got None.", - "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/basic_include/integrations/adr_0007_3.yaml, line 3: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.", + "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/basic_include/configuration.yaml, line 3: required key not provided 'adr_0007_2->host', got None.", + "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/basic_include/integrations/adr_0007_3.yaml, line 3: expected int for dictionary value 'adr_0007_3->port', got 'foo'.", ]) # --- # name: test_component_config_validation_error[include_dir_list] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_2.yaml, line 2: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_3.yaml, line 3: expected str for dictionary value @ data['option1']. Got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_2.yaml, line 2: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_3.yaml, line 3: expected str for dictionary value 'option1', got 123.", "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_4.yaml, line 3: 'no_such_option' is an invalid option for [iot_domain.non_adr_0007], check: no_such_option", ]) # --- # name: test_component_config_validation_error[include_dir_merge_list] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_1.yaml, line 5: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 3: expected str for dictionary value @ data['option1']. Got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_1.yaml, line 5: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 3: expected str for dictionary value 'option1', got 123.", "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 6: 'no_such_option' is an invalid option for [iot_domain.non_adr_0007], check: no_such_option", ]) # --- # name: test_component_config_validation_error[packages] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 11: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 16: expected str for dictionary value @ data['option1']. Got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 11: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 16: expected str for dictionary value 'option1', got 123.", "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 21: 'no_such_option' is an invalid option for [iot_domain.non_adr_0007], check: no_such_option", - "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 28: required key not provided @ data['adr_0007_2']['host']. Got None.", - "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 33: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.", + "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 28: required key not provided 'adr_0007_2->host', got None.", + "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 33: expected int for dictionary value 'adr_0007_3->port', got 'foo'.", "Invalid config for [adr_0007_4] at /fixtures/core/config/component_validation/packages/configuration.yaml, line 38: 'no_such_option' is an invalid option for [adr_0007_4], check: adr_0007_4->no_such_option", ]) # --- # name: test_component_config_validation_error[packages_include_dir_named] list([ - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 6: required key not provided @ data['platform']. Got None.", - "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 9: expected str for dictionary value @ data['option1']. Got 123.", - "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 12: required key not provided @ data['platform']. Got None.", - "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/adr_0007_2.yaml, line 2: required key not provided @ data['adr_0007_2']['host']. Got None.", - "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/adr_0007_3.yaml, line 4: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 6: required key not provided 'platform', got None.", + "Invalid config for [iot_domain.non_adr_0007] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 9: expected str for dictionary value 'option1', got 123.", + "Invalid config for [iot_domain] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 12: required key not provided 'platform', got None.", + "Invalid config for [adr_0007_2] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/adr_0007_2.yaml, line 2: required key not provided 'adr_0007_2->host', got None.", + "Invalid config for [adr_0007_3] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/adr_0007_3.yaml, line 4: expected int for dictionary value 'adr_0007_3->port', got 'foo'.", "Invalid config for [adr_0007_4] at /fixtures/core/config/component_validation/packages_include_dir_named/integrations/adr_0007_4.yaml, line 4: 'no_such_option' is an invalid option for [adr_0007_4], check: adr_0007_4->no_such_option", ]) # ---