mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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,7 +4,9 @@ from __future__ import annotations
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from functools import reduce
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
@ -505,6 +507,77 @@ def async_log_exception(
|
|||||||
_LOGGER.error(message, exc_info=not is_friendly and ex)
|
_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
|
@callback
|
||||||
def _format_config_error(
|
def _format_config_error(
|
||||||
ex: Exception, domain: str, config: dict, link: str | None = None
|
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.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
is_friendly = False
|
is_friendly = False
|
||||||
message = f"Invalid config for [{domain}]: "
|
message = f"Invalid config for [{domain}]"
|
||||||
|
|
||||||
if isinstance(ex, vol.Invalid):
|
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:
|
if "extra keys not allowed" in ex.error_message:
|
||||||
path = "->".join(str(m) for m in ex.path)
|
path = "->".join(str(m) for m in ex.path)
|
||||||
message += (
|
message += (
|
||||||
f"[{ex.path[-1]}] is an invalid option for [{domain}]. "
|
f"'{ex.path[-1]}' is an invalid option for [{domain}], check: {path}"
|
||||||
f"Check: {domain}->{path}."
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
message += f"{humanize_error(config, ex)}."
|
message += f"{humanize_error(config, ex)}."
|
||||||
is_friendly = True
|
is_friendly = True
|
||||||
else:
|
else:
|
||||||
|
message += ": "
|
||||||
message += str(ex) or repr(ex)
|
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:
|
if domain != CONF_CORE and link:
|
||||||
message += f" Please check the docs at {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": {"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(
|
error = CheckConfigError(
|
||||||
(
|
(
|
||||||
"Invalid config for [homeassistant]: not a valid value for dictionary "
|
"Invalid config for [homeassistant] at "
|
||||||
"value @ data['unit_system']. Got 'bad'. (See "
|
f"{hass.config.path(YAML_CONFIG_FILE)}, line 2: "
|
||||||
f"{hass.config.path(YAML_CONFIG_FILE)}, line 2)."
|
"not a valid value for dictionary value @ data['unit_system']. Got "
|
||||||
|
"'bad'."
|
||||||
),
|
),
|
||||||
"homeassistant",
|
"homeassistant",
|
||||||
{"unit_system": "bad"},
|
{"unit_system": "bad"},
|
||||||
@ -190,9 +191,9 @@ async def test_component_import_error(hass: HomeAssistant) -> None:
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("component", "errors", "warnings", "message"),
|
("component", "errors", "warnings", "message"),
|
||||||
[
|
[
|
||||||
("frontend", 1, 0, "[blah] is an invalid option for [frontend]"),
|
("frontend", 1, 0, "'blah' is an invalid option for [frontend]"),
|
||||||
("http", 1, 0, "[blah] is an invalid option for [http]"),
|
("http", 1, 0, "'blah' is an invalid option for [http]"),
|
||||||
("logger", 0, 1, "[blah] is an invalid option for [logger]"),
|
("logger", 0, 1, "'blah' is an invalid option for [logger]"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_component_schema_error(
|
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",
|
"blah:\n - platform: test\n option1: 123",
|
||||||
1,
|
1,
|
||||||
"Invalid config for [blah.test]: expected str for dictionary value",
|
"expected str for dictionary value",
|
||||||
{"option1": 123, "platform": "test"},
|
{"option1": 123, "platform": "test"},
|
||||||
),
|
),
|
||||||
# Test the attached config is unvalidated (key old is removed by validator)
|
# Test the attached config is unvalidated (key old is removed by validator)
|
||||||
(
|
(
|
||||||
"blah:\n - platform: test\n old: blah\n option1: 123",
|
"blah:\n - platform: test\n old: blah\n option1: 123",
|
||||||
1,
|
1,
|
||||||
"Invalid config for [blah.test]: expected str for dictionary value",
|
"expected str for dictionary value",
|
||||||
{"old": "blah", "option1": 123, "platform": "test"},
|
{"old": "blah", "option1": 123, "platform": "test"},
|
||||||
),
|
),
|
||||||
# Test base platform configuration error
|
# Test base platform configuration error
|
||||||
(
|
(
|
||||||
"blah:\n - paltfrom: test\n",
|
"blah:\n - paltfrom: test\n",
|
||||||
1,
|
1,
|
||||||
"Invalid config for [blah]: required key not provided",
|
"required key not provided",
|
||||||
{"paltfrom": "test"},
|
{"paltfrom": "test"},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,46 +1,46 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_component_config_validation_error[basic]
|
# name: test_component_config_validation_error[basic]
|
||||||
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/basic/configuration.yaml, line 6).",
|
"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]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/basic/configuration.yaml, line 9).",
|
"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]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
"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]: 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 [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]
|
# name: test_component_config_validation_error[basic_include]
|
||||||
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/basic_include/integrations/iot_domain.yaml, line 5).",
|
"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]: 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 [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]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
"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]: 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 [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]
|
# name: test_component_config_validation_error[include_dir_list]
|
||||||
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.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]: 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] 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]
|
# name: test_component_config_validation_error[include_dir_merge_list]
|
||||||
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.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]: 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] 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]
|
# name: test_component_config_validation_error[packages]
|
||||||
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/packages/configuration.yaml, line 11).",
|
"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]: required key not provided @ data['platform']. Got None. (See <BASE_PATH>/fixtures/core/config/component_validation/packages/configuration.yaml, line 16).",
|
"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]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
"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]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See ?, line ?).",
|
"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]
|
# name: test_component_config_validation_error[packages_include_dir_named]
|
||||||
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/packages_include_dir_named/integrations/iot_domain.yaml, line 6).",
|
"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]: 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 [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]: required key not provided @ data['adr_0007_2']['host']. Got None. (See ?, line ?).",
|
"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]: expected int for dictionary value @ data['adr_0007_3']['port']. Got 'foo'. (See ?, line ?).",
|
"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]
|
# name: test_package_merge_error[packages]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user