From 9f0d3bfce81601df642f96f1ef43e6cac952f147 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 May 2023 19:11:39 -0500 Subject: [PATCH] Use ReadOnlyDict for entity registry options (#93824) * Use ReadOnlyDict for entity registry options While reviewing #93601 it was noticed this was slow at startup https://github.com/home-assistant/core/pull/93601#issuecomment-1568958280 This is a first pass attempt to improve the performance * fix tests --- homeassistant/helpers/entity_registry.py | 37 +++++++------------ .../snapshots/test_exposed_entities.ambr | 2 +- tests/helpers/test_entity_registry.py | 8 ++-- tests/syrupy.py | 2 +- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d8c5a6c1cf6..b6fc84b9627 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -12,7 +12,6 @@ from __future__ import annotations from collections import UserDict from collections.abc import Callable, Iterable, Mapping, ValuesView import logging -from types import MappingProxyType from typing import TYPE_CHECKING, Any, TypeVar, cast import attr @@ -44,6 +43,7 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.util import slugify, uuid as uuid_util from homeassistant.util.json import format_unserializable_data +from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -102,6 +102,7 @@ class RegistryEntryHider(StrEnum): EntityOptionsType = Mapping[str, Mapping[str, Any]] +ReadOnlyEntityOptionsType = ReadOnlyDict[str, Mapping[str, Any]] DISLAY_DICT_OPTIONAL = ( ("ai", "area_id"), @@ -110,27 +111,13 @@ DISLAY_DICT_OPTIONAL = ( ) -class _EntityOptions(UserDict[str, MappingProxyType]): - """Container for entity options.""" - - def __init__(self, data: Mapping[str, Mapping] | None) -> None: - """Initialize.""" - super().__init__() - if data is None: - return - self.data = {key: MappingProxyType(val) for key, val in data.items()} - - def __setitem__(self, key: str, entry: Mapping) -> None: - """Add an item.""" - raise NotImplementedError - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - raise NotImplementedError - - def as_dict(self) -> dict[str, dict]: - """Return dictionary version.""" - return {key: dict(val) for key, val in self.data.items()} +def _protect_entity_options( + data: EntityOptionsType | None, +) -> ReadOnlyEntityOptionsType: + """Protect entity options from being modified.""" + if data is None: + return ReadOnlyDict({}) + return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()}) @attr.s(slots=True, frozen=True) @@ -154,7 +141,9 @@ class RegistryEntry: id: str = attr.ib(factory=uuid_util.random_uuid_hex) has_entity_name: bool = attr.ib(default=False) name: str | None = attr.ib(default=None) - options: _EntityOptions = attr.ib(default=None, converter=_EntityOptions) + options: ReadOnlyEntityOptionsType = attr.ib( + default=None, converter=_protect_entity_options + ) # As set by integration original_device_class: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) @@ -1029,7 +1018,7 @@ class EntityRegistry: "id": entry.id, "has_entity_name": entry.has_entity_name, "name": entry.name, - "options": entry.options.as_dict(), + "options": entry.options, "original_device_class": entry.original_device_class, "original_icon": entry.original_icon, "original_name": entry.original_name, diff --git a/tests/components/homeassistant/snapshots/test_exposed_entities.ambr b/tests/components/homeassistant/snapshots/test_exposed_entities.ambr index 2f9d0b8017f..55b95186b49 100644 --- a/tests/components/homeassistant/snapshots/test_exposed_entities.ambr +++ b/tests/components/homeassistant/snapshots/test_exposed_entities.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_get_assistant_settings dict({ - 'climate.test_unique1': mappingproxy({ + 'climate.test_unique1': ReadOnlyDict({ 'should_expose': True, }), 'light.not_in_registry': dict({ diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e3b91c46e18..a0425065775 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -748,13 +748,13 @@ async def test_update_entity_options(entity_registry: er.EntityRegistry) -> None assert new_entry_1.options == {"light": {"minimum_brightness": 20}} # Test it's not possible to modify the options - with pytest.raises(NotImplementedError): + with pytest.raises(RuntimeError): new_entry_1.options["blah"] = {} - with pytest.raises(NotImplementedError): + with pytest.raises(RuntimeError): new_entry_1.options["light"] = {} - with pytest.raises(TypeError): + with pytest.raises(RuntimeError): new_entry_1.options["light"]["blah"] = 123 - with pytest.raises(TypeError): + with pytest.raises(RuntimeError): new_entry_1.options["light"]["minimum_brightness"] = 123 entity_registry.async_update_entity_options( diff --git a/tests/syrupy.py b/tests/syrupy.py index af34cb628fc..9433eb1649c 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -170,7 +170,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): "config_entry_id": ANY, "device_id": ANY, "id": ANY, - "options": data.options.as_dict(), + "options": {k: dict(v) for k, v in data.options.items()}, } ) serialized.pop("_partial_repr")