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
This commit is contained in:
J. Nick Koston 2023-05-30 19:11:39 -05:00 committed by GitHub
parent 31e217a11e
commit 9f0d3bfce8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 19 additions and 30 deletions

View file

@ -12,7 +12,6 @@ from __future__ import annotations
from collections import UserDict from collections import UserDict
from collections.abc import Callable, Iterable, Mapping, ValuesView from collections.abc import Callable, Iterable, Mapping, ValuesView
import logging import logging
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypeVar, cast from typing import TYPE_CHECKING, Any, TypeVar, cast
import attr import attr
@ -44,6 +43,7 @@ from homeassistant.core import (
from homeassistant.exceptions import MaxLengthExceeded from homeassistant.exceptions import MaxLengthExceeded
from homeassistant.util import slugify, uuid as uuid_util from homeassistant.util import slugify, uuid as uuid_util
from homeassistant.util.json import format_unserializable_data from homeassistant.util.json import format_unserializable_data
from homeassistant.util.read_only_dict import ReadOnlyDict
from . import device_registry as dr, storage from . import device_registry as dr, storage
from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED
@ -102,6 +102,7 @@ class RegistryEntryHider(StrEnum):
EntityOptionsType = Mapping[str, Mapping[str, Any]] EntityOptionsType = Mapping[str, Mapping[str, Any]]
ReadOnlyEntityOptionsType = ReadOnlyDict[str, Mapping[str, Any]]
DISLAY_DICT_OPTIONAL = ( DISLAY_DICT_OPTIONAL = (
("ai", "area_id"), ("ai", "area_id"),
@ -110,27 +111,13 @@ DISLAY_DICT_OPTIONAL = (
) )
class _EntityOptions(UserDict[str, MappingProxyType]): def _protect_entity_options(
"""Container for entity options.""" data: EntityOptionsType | None,
) -> ReadOnlyEntityOptionsType:
def __init__(self, data: Mapping[str, Mapping] | None) -> None: """Protect entity options from being modified."""
"""Initialize.""" if data is None:
super().__init__() return ReadOnlyDict({})
if data is None: return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()})
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()}
@attr.s(slots=True, frozen=True) @attr.s(slots=True, frozen=True)
@ -154,7 +141,9 @@ class RegistryEntry:
id: str = attr.ib(factory=uuid_util.random_uuid_hex) id: str = attr.ib(factory=uuid_util.random_uuid_hex)
has_entity_name: bool = attr.ib(default=False) has_entity_name: bool = attr.ib(default=False)
name: str | None = attr.ib(default=None) 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 # As set by integration
original_device_class: str | None = attr.ib(default=None) original_device_class: str | None = attr.ib(default=None)
original_icon: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None)
@ -1029,7 +1018,7 @@ class EntityRegistry:
"id": entry.id, "id": entry.id,
"has_entity_name": entry.has_entity_name, "has_entity_name": entry.has_entity_name,
"name": entry.name, "name": entry.name,
"options": entry.options.as_dict(), "options": entry.options,
"original_device_class": entry.original_device_class, "original_device_class": entry.original_device_class,
"original_icon": entry.original_icon, "original_icon": entry.original_icon,
"original_name": entry.original_name, "original_name": entry.original_name,

View file

@ -1,7 +1,7 @@
# serializer version: 1 # serializer version: 1
# name: test_get_assistant_settings # name: test_get_assistant_settings
dict({ dict({
'climate.test_unique1': mappingproxy({ 'climate.test_unique1': ReadOnlyDict({
'should_expose': True, 'should_expose': True,
}), }),
'light.not_in_registry': dict({ 'light.not_in_registry': dict({

View file

@ -748,13 +748,13 @@ async def test_update_entity_options(entity_registry: er.EntityRegistry) -> None
assert new_entry_1.options == {"light": {"minimum_brightness": 20}} assert new_entry_1.options == {"light": {"minimum_brightness": 20}}
# Test it's not possible to modify the options # Test it's not possible to modify the options
with pytest.raises(NotImplementedError): with pytest.raises(RuntimeError):
new_entry_1.options["blah"] = {} new_entry_1.options["blah"] = {}
with pytest.raises(NotImplementedError): with pytest.raises(RuntimeError):
new_entry_1.options["light"] = {} new_entry_1.options["light"] = {}
with pytest.raises(TypeError): with pytest.raises(RuntimeError):
new_entry_1.options["light"]["blah"] = 123 new_entry_1.options["light"]["blah"] = 123
with pytest.raises(TypeError): with pytest.raises(RuntimeError):
new_entry_1.options["light"]["minimum_brightness"] = 123 new_entry_1.options["light"]["minimum_brightness"] = 123
entity_registry.async_update_entity_options( entity_registry.async_update_entity_options(

View file

@ -170,7 +170,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
"config_entry_id": ANY, "config_entry_id": ANY,
"device_id": ANY, "device_id": ANY,
"id": ANY, "id": ANY,
"options": data.options.as_dict(), "options": {k: dict(v) for k, v in data.options.items()},
} }
) )
serialized.pop("_partial_repr") serialized.pop("_partial_repr")