From 3f32c5d2addafc53c6621f5c7bfa793057b1667c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Mar 2023 12:29:57 -0500 Subject: [PATCH] Yaml use dict (#88977) * Use built-in dict instead of OrderedDict * Use dict instead of OrderedDict in YAML --- homeassistant/util/yaml/dumper.py | 7 +- homeassistant/util/yaml/loader.py | 21 +- homeassistant/util/yaml/objects.py | 4 + .../blueprint/snapshots/test_importer.ambr | 338 +++++++++--------- 4 files changed, 190 insertions(+), 180 deletions(-) diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index db8b496d90e..a3fba653042 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, NodeListClass +from .objects import Input, NodeDictClass, NodeListClass # mypy: allow-untyped-calls, no-warn-return-any @@ -74,6 +74,11 @@ add_representer( lambda dumper, value: represent_odict(dumper, "tag:yaml.org,2002:map", value), ) +add_representer( + NodeDictClass, + lambda dumper, value: represent_odict(dumper, "tag:yaml.org,2002:map", value), +) + add_representer( NodeListClass, lambda dumper, value: dumper.represent_sequence("tag:yaml.org,2002:seq", value), diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index bf8a4e9541a..b5840a79e8d 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -1,7 +1,6 @@ """Custom loader.""" from __future__ import annotations -from collections import OrderedDict from collections.abc import Iterator import fnmatch from io import StringIO, TextIOWrapper @@ -25,7 +24,7 @@ except ImportError: from homeassistant.exceptions import HomeAssistantError from .const import SECRET_YAML -from .objects import Input, NodeListClass, NodeStrClass +from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any @@ -205,7 +204,7 @@ def _parse_yaml( # We convert that to an empty dict return ( yaml.load(content, Loader=lambda stream: loader(stream, secrets)) - or OrderedDict() + or NodeDictClass() ) @@ -276,9 +275,9 @@ def _find_files(directory: str, pattern: str) -> Iterator[str]: yield filename -def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> OrderedDict: +def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> NodeDictClass: """Load multiple files from directory as a dictionary.""" - mapping: OrderedDict = OrderedDict() + mapping = NodeDictClass() loc = os.path.join(os.path.dirname(loader.get_name()), node.value) for fname in _find_files(loc, "*.yaml"): filename = os.path.splitext(os.path.basename(fname))[0] @@ -290,9 +289,9 @@ def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> Ordere def _include_dir_merge_named_yaml( loader: LoaderType, node: yaml.nodes.Node -) -> OrderedDict: +) -> NodeDictClass: """Load multiple files from directory as a merged dictionary.""" - mapping: OrderedDict = OrderedDict() + mapping = NodeDictClass() loc = os.path.join(os.path.dirname(loader.get_name()), node.value) for fname in _find_files(loc, "*.yaml"): if os.path.basename(fname) == SECRET_YAML: @@ -330,7 +329,9 @@ def _include_dir_merge_list_yaml( return _add_reference(merged_list, loader, node) -def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDict: +def _handle_mapping_tag( + loader: LoaderType, node: yaml.nodes.MappingNode +) -> NodeDictClass: """Load YAML mappings into an ordered dictionary to preserve key order.""" loader.flatten_mapping(node) nodes = loader.construct_pairs(node) @@ -361,7 +362,7 @@ def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDi ) seen[key] = line - return _add_reference(OrderedDict(nodes), loader, node) + return _add_reference(NodeDictClass(nodes), loader, node) def _construct_seq(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: @@ -398,7 +399,7 @@ def add_constructor(tag: Any, constructor: Any) -> None: add_constructor("!include", _include_yaml) -add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) +add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _handle_mapping_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/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index 2d318a9def0..e7b262ad496 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -14,6 +14,10 @@ class NodeStrClass(str): """Wrapper class to be able to add attributes on a string.""" +class NodeDictClass(dict): + """Wrapper class to be able to add attributes on a dict.""" + + @dataclass(frozen=True) class Input: """Input that should be substituted.""" diff --git a/tests/components/blueprint/snapshots/test_importer.ambr b/tests/components/blueprint/snapshots/test_importer.ambr index 6e5648b54d9..002d5204dc8 100644 --- a/tests/components/blueprint/snapshots/test_importer.ambr +++ b/tests/components/blueprint/snapshots/test_importer.ambr @@ -1,25 +1,79 @@ # serializer version: 1 # name: test_extract_blueprint_from_community_topic - OrderedDict({ - 'remote': OrderedDict({ - 'name': 'Remote', - 'description': 'IKEA remote to use', + NodeDictClass({ + 'brightness': NodeDictClass({ + 'default': 50, + 'description': 'Brightness of the light(s) when turning on', + 'name': 'Brightness', 'selector': dict({ - 'device': OrderedDict({ - 'integration': 'zha', - 'manufacturer': 'IKEA of Sweden', - 'model': 'TRADFRI remote control', - 'multiple': False, + 'number': NodeDictClass({ + 'max': 100.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 1.0, + 'unit_of_measurement': '%', }), }), }), - 'light': OrderedDict({ - 'name': 'Light(s)', - 'description': 'The light(s) to control', + 'button_left_long': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on long left button press', + 'name': 'Left button - long press', 'selector': dict({ - 'target': OrderedDict({ + 'action': dict({ + }), + }), + }), + 'button_left_short': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on short left button press', + 'name': 'Left button - short press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'button_right_long': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on long right button press', + 'name': 'Right button - long press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'button_right_short': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on short right button press', + 'name': 'Right button - short press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'force_brightness': NodeDictClass({ + 'default': False, + 'description': ''' + Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on. + + ''', + 'name': 'Force turn on brightness', + 'selector': dict({ + 'boolean': dict({ + }), + }), + }), + 'light': NodeDictClass({ + 'description': 'The light(s) to control', + 'name': 'Light(s)', + 'selector': dict({ + 'target': NodeDictClass({ 'entity': list([ - OrderedDict({ + NodeDictClass({ 'domain': list([ 'light', ]), @@ -28,95 +82,95 @@ }), }), }), - 'force_brightness': OrderedDict({ - 'name': 'Force turn on brightness', - 'description': ''' - Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on. - - ''', - 'default': False, + 'remote': NodeDictClass({ + 'description': 'IKEA remote to use', + 'name': 'Remote', 'selector': dict({ - 'boolean': dict({ - }), - }), - }), - 'brightness': OrderedDict({ - 'name': 'Brightness', - 'description': 'Brightness of the light(s) when turning on', - 'default': 50, - 'selector': dict({ - 'number': OrderedDict({ - 'min': 0.0, - 'max': 100.0, - 'mode': 'slider', - 'step': 1.0, - 'unit_of_measurement': '%', - }), - }), - }), - 'button_left_short': OrderedDict({ - 'name': 'Left button - short press', - 'description': 'Action to run on short left button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_left_long': OrderedDict({ - 'name': 'Left button - long press', - 'description': 'Action to run on long left button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_right_short': OrderedDict({ - 'name': 'Right button - short press', - 'description': 'Action to run on short right button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_right_long': OrderedDict({ - 'name': 'Right button - long press', - 'description': 'Action to run on long right button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ + 'device': NodeDictClass({ + 'integration': 'zha', + 'manufacturer': 'IKEA of Sweden', + 'model': 'TRADFRI remote control', + 'multiple': False, }), }), }), }) # --- # name: test_fetch_blueprint_from_community_url - OrderedDict({ - 'remote': OrderedDict({ - 'name': 'Remote', - 'description': 'IKEA remote to use', + NodeDictClass({ + 'brightness': NodeDictClass({ + 'default': 50, + 'description': 'Brightness of the light(s) when turning on', + 'name': 'Brightness', 'selector': dict({ - 'device': OrderedDict({ - 'integration': 'zha', - 'manufacturer': 'IKEA of Sweden', - 'model': 'TRADFRI remote control', - 'multiple': False, + 'number': NodeDictClass({ + 'max': 100.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 1.0, + 'unit_of_measurement': '%', }), }), }), - 'light': OrderedDict({ - 'name': 'Light(s)', - 'description': 'The light(s) to control', + 'button_left_long': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on long left button press', + 'name': 'Left button - long press', 'selector': dict({ - 'target': OrderedDict({ + 'action': dict({ + }), + }), + }), + 'button_left_short': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on short left button press', + 'name': 'Left button - short press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'button_right_long': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on long right button press', + 'name': 'Right button - long press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'button_right_short': NodeDictClass({ + 'default': NodeListClass([ + ]), + 'description': 'Action to run on short right button press', + 'name': 'Right button - short press', + 'selector': dict({ + 'action': dict({ + }), + }), + }), + 'force_brightness': NodeDictClass({ + 'default': False, + 'description': ''' + Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on. + + ''', + 'name': 'Force turn on brightness', + 'selector': dict({ + 'boolean': dict({ + }), + }), + }), + 'light': NodeDictClass({ + 'description': 'The light(s) to control', + 'name': 'Light(s)', + 'selector': dict({ + 'target': NodeDictClass({ 'entity': list([ - OrderedDict({ + NodeDictClass({ 'domain': list([ 'light', ]), @@ -125,94 +179,26 @@ }), }), }), - 'force_brightness': OrderedDict({ - 'name': 'Force turn on brightness', - 'description': ''' - Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on. - - ''', - 'default': False, + 'remote': NodeDictClass({ + 'description': 'IKEA remote to use', + 'name': 'Remote', 'selector': dict({ - 'boolean': dict({ - }), - }), - }), - 'brightness': OrderedDict({ - 'name': 'Brightness', - 'description': 'Brightness of the light(s) when turning on', - 'default': 50, - 'selector': dict({ - 'number': OrderedDict({ - 'min': 0.0, - 'max': 100.0, - 'mode': 'slider', - 'step': 1.0, - 'unit_of_measurement': '%', - }), - }), - }), - 'button_left_short': OrderedDict({ - 'name': 'Left button - short press', - 'description': 'Action to run on short left button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_left_long': OrderedDict({ - 'name': 'Left button - long press', - 'description': 'Action to run on long left button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_right_short': OrderedDict({ - 'name': 'Right button - short press', - 'description': 'Action to run on short right button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ - }), - }), - }), - 'button_right_long': OrderedDict({ - 'name': 'Right button - long press', - 'description': 'Action to run on long right button press', - 'default': NodeListClass([ - ]), - 'selector': dict({ - 'action': dict({ + 'device': NodeDictClass({ + 'integration': 'zha', + 'manufacturer': 'IKEA of Sweden', + 'model': 'TRADFRI remote control', + 'multiple': False, }), }), }), }) # --- # name: test_fetch_blueprint_from_github_gist_url - OrderedDict({ - 'motion_entity': OrderedDict({ - 'name': 'Motion Sensor', - 'selector': dict({ - 'entity': OrderedDict({ - 'domain': list([ - 'binary_sensor', - ]), - 'device_class': list([ - 'motion', - ]), - 'multiple': False, - }), - }), - }), - 'light_entity': OrderedDict({ + NodeDictClass({ + 'light_entity': NodeDictClass({ 'name': 'Light', 'selector': dict({ - 'entity': OrderedDict({ + 'entity': NodeDictClass({ 'domain': list([ 'light', ]), @@ -220,5 +206,19 @@ }), }), }), + 'motion_entity': NodeDictClass({ + 'name': 'Motion Sensor', + 'selector': dict({ + 'entity': NodeDictClass({ + 'device_class': list([ + 'motion', + ]), + 'domain': list([ + 'binary_sensor', + ]), + 'multiple': False, + }), + }), + }), }) # ---