diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index a3fba653042..65747d1fd3e 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -4,7 +4,7 @@ from typing import Any import yaml -from .objects import Input, NodeDictClass, NodeListClass +from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any @@ -84,6 +84,11 @@ add_representer( lambda dumper, value: dumper.represent_sequence("tag:yaml.org,2002:seq", value), ) +add_representer( + NodeStrClass, + lambda dumper, value: dumper.represent_scalar("tag:yaml.org,2002:str", str(value)), +) + add_representer( Input, lambda dumper, value: dumper.represent_scalar("!input", value.name), diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 5f18a729130..75942e5ea79 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -371,6 +371,16 @@ def _construct_seq(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: return _add_reference(obj, loader, node) +def _handle_scalar_tag( + loader: LoaderType, node: yaml.nodes.ScalarNode +) -> str | int | float | None: + """Add line number and file name to Load YAML sequence.""" + obj = loader.construct_scalar(node) + if not isinstance(obj, str): + return obj + return _add_reference(obj, loader, node) + + def _env_var_yaml(loader: LoaderType, node: yaml.nodes.Node) -> str: """Load environment variables and embed it into the configuration YAML.""" args = node.value.split() @@ -400,6 +410,7 @@ def add_constructor(tag: Any, constructor: Any) -> None: add_constructor("!include", _include_yaml) add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _handle_mapping_tag) +add_constructor(yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, _handle_scalar_tag) add_constructor(yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq) add_constructor("!env_var", _env_var_yaml) add_constructor("!secret", secret_yaml) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 4f60c5836b5..53b3143ac0b 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -547,3 +547,36 @@ async def test_loading_actual_file_with_syntax( "fixtures", "bad.yaml.txt" ) await hass.async_add_executor_job(load_yaml_config_file, fixture_path) + + +def test_string_annotated(try_both_loaders) -> None: + """Test strings are annotated with file + line.""" + conf = ( + "key1: str\n" + "key2:\n" + " blah: blah\n" + "key3:\n" + " - 1\n" + " - 2\n" + " - 3\n" + "key4: yes\n" + "key5: 1\n" + "key6: 1.0\n" + ) + expected_annotations = { + "key1": [("", 0), ("", 0)], + "key2": [("", 1), ("", 2)], + "key3": [("", 3), ("", 4)], + "key4": [("", 7), (None, None)], + "key5": [("", 8), (None, None)], + "key6": [("", 9), (None, None)], + } + with io.StringIO(conf) as file: + doc = yaml_loader.parse_yaml(file) + for key, value in doc.items(): + assert getattr(key, "__config_file__", None) == expected_annotations[key][0][0] + assert getattr(key, "__line__", None) == expected_annotations[key][0][1] + assert ( + getattr(value, "__config_file__", None) == expected_annotations[key][1][0] + ) + assert getattr(value, "__line__", None) == expected_annotations[key][1][1]