Improve print of line numbers when there are configuration errors (#103216)
* Improve print of line numbers when there are configuration errors * Update alarm_control_panel test
This commit is contained in:
parent
9241554d45
commit
dedd3418a1
4 changed files with 113 additions and 43 deletions
|
@ -4,7 +4,9 @@ from __future__ import annotations
|
|||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Sequence
|
||||
from contextlib import suppress
|
||||
from functools import reduce
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
@ -505,6 +507,77 @@ def async_log_exception(
|
|||
_LOGGER.error(message, exc_info=not is_friendly and ex)
|
||||
|
||||
|
||||
def _get_annotation(item: Any) -> tuple[str, int | str] | None:
|
||||
if not hasattr(item, "__config_file__"):
|
||||
return None
|
||||
|
||||
return (getattr(item, "__config_file__"), getattr(item, "__line__", "?"))
|
||||
|
||||
|
||||
def _get_by_path(data: dict | list, items: list[str | int]) -> Any:
|
||||
"""Access a nested object in root by item sequence.
|
||||
|
||||
Returns None in case of error.
|
||||
"""
|
||||
try:
|
||||
return reduce(operator.getitem, items, data) # type: ignore[arg-type]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def find_annotation(
|
||||
config: dict | list, path: list[str | int]
|
||||
) -> tuple[str, int | str] | None:
|
||||
"""Find file/line annotation for a node in config pointed to by path.
|
||||
|
||||
If the node pointed to is a dict or list, prefer the annotation for the key in
|
||||
the key/value pair defining the dict or list.
|
||||
If the node is not annotated, try the parent node.
|
||||
"""
|
||||
|
||||
def find_annotation_for_key(
|
||||
item: dict, path: list[str | int], tail: str | int
|
||||
) -> tuple[str, int | str] | None:
|
||||
for key in item:
|
||||
if key == tail:
|
||||
if annotation := _get_annotation(key):
|
||||
return annotation
|
||||
break
|
||||
return None
|
||||
|
||||
def find_annotation_rec(
|
||||
config: dict | list, path: list[str | int], tail: str | int | None
|
||||
) -> tuple[str, int | str] | None:
|
||||
item = _get_by_path(config, path)
|
||||
if isinstance(item, dict) and tail is not None:
|
||||
if tail_annotation := find_annotation_for_key(item, path, tail):
|
||||
return tail_annotation
|
||||
|
||||
if (
|
||||
isinstance(item, (dict, list))
|
||||
and path
|
||||
and (
|
||||
key_annotation := find_annotation_for_key(
|
||||
_get_by_path(config, path[:-1]), path[:-1], path[-1]
|
||||
)
|
||||
)
|
||||
):
|
||||
return key_annotation
|
||||
|
||||
if annotation := _get_annotation(item):
|
||||
return annotation
|
||||
|
||||
if not path:
|
||||
return None
|
||||
|
||||
tail = path.pop()
|
||||
if annotation := find_annotation_rec(config, path, tail):
|
||||
return annotation
|
||||
return _get_annotation(item)
|
||||
|
||||
return find_annotation_rec(config, list(path), None)
|
||||
|
||||
|
||||
@callback
|
||||
def _format_config_error(
|
||||
ex: Exception, domain: str, config: dict, link: str | None = None
|
||||
|
@ -514,30 +587,26 @@ def _format_config_error(
|
|||
This method must be run in the event loop.
|
||||
"""
|
||||
is_friendly = False
|
||||
message = f"Invalid config for [{domain}]: "
|
||||
message = f"Invalid config for [{domain}]"
|
||||
|
||||
if isinstance(ex, vol.Invalid):
|
||||
if annotation := find_annotation(config, ex.path):
|
||||
message += f" at {annotation[0]}, line {annotation[1]}: "
|
||||
else:
|
||||
message += ": "
|
||||
|
||||
if "extra keys not allowed" in ex.error_message:
|
||||
path = "->".join(str(m) for m in ex.path)
|
||||
message += (
|
||||
f"[{ex.path[-1]}] is an invalid option for [{domain}]. "
|
||||
f"Check: {domain}->{path}."
|
||||
f"'{ex.path[-1]}' is an invalid option for [{domain}], check: {path}"
|
||||
)
|
||||
else:
|
||||
message += f"{humanize_error(config, ex)}."
|
||||
is_friendly = True
|
||||
else:
|
||||
message += ": "
|
||||
message += str(ex) or repr(ex)
|
||||
|
||||
try:
|
||||
domain_config = config.get(domain, config)
|
||||
except AttributeError:
|
||||
domain_config = config
|
||||
|
||||
message += (
|
||||
f" (See {getattr(domain_config, '__config_file__', '?')}, "
|
||||
f"line {getattr(domain_config, '__line__', '?')})."
|
||||
)
|
||||
|
||||
if domain != CONF_CORE and link:
|
||||
message += f" Please check the docs at {link}"
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ async def test_optimistic_states(hass: HomeAssistant, start_ha) -> None:
|
|||
"wibble": {"test_panel": "Invalid"},
|
||||
}
|
||||
},
|
||||
"[wibble] is an invalid option",
|
||||
"'wibble' is an invalid option",
|
||||
),
|
||||
(
|
||||
{
|
||||
|
|
|
@ -81,9 +81,10 @@ async def test_bad_core_config(hass: HomeAssistant) -> None:
|
|||
|
||||
error = CheckConfigError(
|
||||
(
|
||||
"Invalid config for [homeassistant]: not a valid value for dictionary "
|
||||
"value @ data['unit_system']. Got 'bad'. (See "
|
||||
f"{hass.config.path(YAML_CONFIG_FILE)}, line 2)."
|
||||
"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'."
|
||||
),
|
||||
"homeassistant",
|
||||
{"unit_system": "bad"},
|
||||
|
@ -190,9 +191,9 @@ async def test_component_import_error(hass: HomeAssistant) -> None:
|
|||
@pytest.mark.parametrize(
|
||||
("component", "errors", "warnings", "message"),
|
||||
[
|
||||
("frontend", 1, 0, "[blah] is an invalid option for [frontend]"),
|
||||
("http", 1, 0, "[blah] is an invalid option for [http]"),
|
||||
("logger", 0, 1, "[blah] is an invalid option for [logger]"),
|
||||
("frontend", 1, 0, "'blah' is an invalid option for [frontend]"),
|
||||
("http", 1, 0, "'blah' is an invalid option for [http]"),
|
||||
("logger", 0, 1, "'blah' is an invalid option for [logger]"),
|
||||
],
|
||||
)
|
||||
async def test_component_schema_error(
|
||||
|
@ -274,21 +275,21 @@ async def test_platform_not_found_safe_mode(hass: HomeAssistant) -> None:
|
|||
(
|
||||
"blah:\n - platform: test\n option1: 123",
|
||||
1,
|
||||
"Invalid config for [blah.test]: expected str for dictionary value",
|
||||
"expected str for dictionary value",
|
||||
{"option1": 123, "platform": "test"},
|
||||
),
|
||||
# Test the attached config is unvalidated (key old is removed by validator)
|
||||
(
|
||||
"blah:\n - platform: test\n old: blah\n option1: 123",
|
||||
1,
|
||||
"Invalid config for [blah.test]: expected str for dictionary value",
|
||||
"expected str for dictionary value",
|
||||
{"old": "blah", "option1": 123, "platform": "test"},
|
||||
),
|
||||
# Test base platform configuration error
|
||||
(
|
||||
"blah:\n - paltfrom: test\n",
|
||||
1,
|
||||
"Invalid config for [blah]: required key not provided",
|
||||
"required key not provided",
|
||||
{"paltfrom": "test"},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
# serializer version: 1
|
||||
# name: test_component_config_validation_error[basic]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 6).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 9).",
|
||||
"Invalid config for [adr_0007_2]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
||||
"Invalid config for [adr_0007_3]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 20).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 7: expected str for dictionary value @ data['option1']. Got 123.",
|
||||
"Invalid config for [iot_domain] at <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 9: required key not provided @ data['platform']. Got None.",
|
||||
"Invalid config for [adr_0007_2] at <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 16: required key not provided @ data['adr_0007_2']['host']. Got None.",
|
||||
"Invalid config for [adr_0007_3] at <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 21: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.",
|
||||
])
|
||||
# ---
|
||||
# name: test_component_config_validation_error[basic_include]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 5).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 8).",
|
||||
"Invalid config for [adr_0007_2]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
||||
"Invalid config for [adr_0007_3]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See <BASE_PATH>/fixtures/core/config/component_validation/basic_include/configuration.yaml, line 4).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 6: expected str for dictionary value @ data['option1']. Got 123.",
|
||||
"Invalid config for [iot_domain] at <BASE_PATH>/fixtures/core/config/component_validation/basic_include/integrations/iot_domain.yaml, line 8: required key not provided @ data['platform']. Got None.",
|
||||
"Invalid config for [adr_0007_2] at <BASE_PATH>/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 <BASE_PATH>/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'.",
|
||||
])
|
||||
# ---
|
||||
# name: test_component_config_validation_error[include_dir_list]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_2.yaml, line 2).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_3.yaml, line 2).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_2.yaml, line 3: expected str for dictionary value @ data['option1']. Got 123.",
|
||||
"Invalid config for [iot_domain] at <BASE_PATH>/fixtures/core/config/component_validation/include_dir_list/iot_domain/iot_domain_3.yaml, line 2: required key not provided @ data['platform']. Got None.",
|
||||
])
|
||||
# ---
|
||||
# name: test_component_config_validation_error[include_dir_merge_list]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 2).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 5).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/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 <BASE_PATH>/fixtures/core/config/component_validation/include_dir_merge_list/iot_domain/iot_domain_2.yaml, line 5: required key not provided @ data['platform']. Got None.",
|
||||
])
|
||||
# ---
|
||||
# name: test_component_config_validation_error[packages]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 11).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 16).",
|
||||
"Invalid config for [adr_0007_2]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
||||
"Invalid config for [adr_0007_3]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See ?, line ?).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 12: expected str for dictionary value @ data['option1']. Got 123.",
|
||||
"Invalid config for [iot_domain] at <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 16: required key not provided @ data['platform']. Got None.",
|
||||
"Invalid config for [adr_0007_2] at <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 23: required key not provided @ data['adr_0007_2']['host']. Got None.",
|
||||
"Invalid config for [adr_0007_3] at <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 28: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'.",
|
||||
])
|
||||
# ---
|
||||
# name: test_component_config_validation_error[packages_include_dir_named]
|
||||
list([
|
||||
"Invalid config for [iot_domain.non_adr_0007]: expected str for dictionary value @ data['option1']. Got 123. (See <BASE_PATH>/fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 6).",
|
||||
"Invalid config for [iot_domain]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 9).",
|
||||
"Invalid config for [adr_0007_2]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
||||
"Invalid config for [adr_0007_3]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See ?, line ?).",
|
||||
"Invalid config for [iot_domain.non_adr_0007] at <BASE_PATH>/fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 7: expected str for dictionary value @ data['option1']. Got 123.",
|
||||
"Invalid config for [iot_domain] at <BASE_PATH>/fixtures/core/config/component_validation/packages_include_dir_named/integrations/iot_domain.yaml, line 9: required key not provided @ data['platform']. Got None.",
|
||||
"Invalid config for [adr_0007_2] at <BASE_PATH>/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 <BASE_PATH>/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'.",
|
||||
])
|
||||
# ---
|
||||
# name: test_package_merge_error[packages]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue