From 24fbc366a68a73959d20992b1c4ce782c2fa8e44 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sat, 5 Oct 2024 05:16:52 -0500
Subject: [PATCH] Restore __slots__ to registry entries (#127481)

---
 .../components/enphase_envoy/diagnostics.py   |  8 +++++--
 homeassistant/helpers/area_registry.py        | 16 +++++++++----
 homeassistant/helpers/device_registry.py      | 18 +++++++++-----
 homeassistant/helpers/entity_registry.py      | 24 ++++++++++++-------
 .../components/device_automation/test_init.py |  2 +-
 .../homekit_controller/test_init.py           |  3 +++
 tests/syrupy.py                               |  3 +++
 7 files changed, 51 insertions(+), 23 deletions(-)

diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py
index b3323687e7c..d5b3880cf24 100644
--- a/homeassistant/components/enphase_envoy/diagnostics.py
+++ b/homeassistant/components/enphase_envoy/diagnostics.py
@@ -104,8 +104,12 @@ async def async_get_config_entry_diagnostics(
             if state := hass.states.get(entity.entity_id):
                 state_dict = dict(state.as_dict())
                 state_dict.pop("context", None)
-            entities.append({"entity": asdict(entity), "state": state_dict})
-        device_entities.append({"device": asdict(device), "entities": entities})
+            entity_dict = asdict(entity)
+            entity_dict.pop("_cache", None)
+            entities.append({"entity": entity_dict, "state": state_dict})
+        device_dict = asdict(device)
+        device_dict.pop("_cache", None)
+        device_entities.append({"device": device_dict, "entities": entities})
 
     # remove envoy serial
     old_serial = coordinator.envoy_serial_number
diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py
index 3f22a54196b..f74296a9fb1 100644
--- a/homeassistant/helpers/area_registry.py
+++ b/homeassistant/helpers/area_registry.py
@@ -7,9 +7,7 @@ from collections.abc import Iterable
 import dataclasses
 from dataclasses import dataclass, field
 from datetime import datetime
-from typing import Any, Literal, TypedDict
-
-from propcache import cached_property
+from typing import TYPE_CHECKING, Any, Literal, TypedDict
 
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.util.dt import utc_from_timestamp, utcnow
@@ -27,6 +25,13 @@ from .singleton import singleton
 from .storage import Store
 from .typing import UNDEFINED, UndefinedType
 
+if TYPE_CHECKING:
+    # mypy cannot workout _cache Protocol with dataclasses
+    from propcache import cached_property as under_cached_property
+else:
+    from propcache import under_cached_property
+
+
 DATA_REGISTRY: HassKey[AreaRegistry] = HassKey("area_registry")
 EVENT_AREA_REGISTRY_UPDATED: EventType[EventAreaRegistryUpdatedData] = EventType(
     "area_registry_updated"
@@ -63,7 +68,7 @@ class EventAreaRegistryUpdatedData(TypedDict):
     area_id: str
 
 
-@dataclass(frozen=True, kw_only=True)
+@dataclass(frozen=True, kw_only=True, slots=True)
 class AreaEntry(NormalizedNameBaseRegistryEntry):
     """Area Registry Entry."""
 
@@ -73,8 +78,9 @@ class AreaEntry(NormalizedNameBaseRegistryEntry):
     id: str
     labels: set[str] = field(default_factory=set)
     picture: str | None
+    _cache: dict[str, Any] = field(default_factory=dict, compare=False, init=False)
 
-    @cached_property
+    @under_cached_property
     def json_fragment(self) -> json_fragment:
         """Return a JSON representation of this AreaEntry."""
         return json_fragment(
diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py
index 0270f819d39..f05179ccf0a 100644
--- a/homeassistant/helpers/device_registry.py
+++ b/homeassistant/helpers/device_registry.py
@@ -12,7 +12,6 @@ import time
 from typing import TYPE_CHECKING, Any, Literal, TypedDict
 
 import attr
-from propcache import cached_property
 from yarl import URL
 
 from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
@@ -46,9 +45,14 @@ from .singleton import singleton
 from .typing import UNDEFINED, UndefinedType
 
 if TYPE_CHECKING:
+    # mypy cannot workout _cache Protocol with attrs
+    from propcache import cached_property as under_cached_property
+
     from homeassistant.config_entries import ConfigEntry
 
     from . import entity_registry
+else:
+    from propcache import under_cached_property
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -278,7 +282,7 @@ def _validate_configuration_url(value: Any) -> str | None:
     return url_as_str
 
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, slots=True)
 class DeviceEntry:
     """Device Registry Entry."""
 
@@ -306,6 +310,7 @@ class DeviceEntry:
     via_device_id: str | None = attr.ib(default=None)
     # This value is not stored, just used to keep track of events to fire.
     is_new: bool = attr.ib(default=False)
+    _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
 
     @property
     def disabled(self) -> bool:
@@ -342,7 +347,7 @@ class DeviceEntry:
             "via_device_id": self.via_device_id,
         }
 
-    @cached_property
+    @under_cached_property
     def json_repr(self) -> bytes | None:
         """Return a cached JSON representation of the entry."""
         try:
@@ -358,7 +363,7 @@ class DeviceEntry:
             )
         return None
 
-    @cached_property
+    @under_cached_property
     def as_storage_fragment(self) -> json_fragment:
         """Return a json fragment for storage."""
         return json_fragment(
@@ -390,7 +395,7 @@ class DeviceEntry:
         )
 
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, slots=True)
 class DeletedDeviceEntry:
     """Deleted Device Registry Entry."""
 
@@ -401,6 +406,7 @@ class DeletedDeviceEntry:
     orphaned_timestamp: float | None = attr.ib()
     created_at: datetime = attr.ib(factory=utcnow)
     modified_at: datetime = attr.ib(factory=utcnow)
+    _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
 
     def to_device_entry(
         self,
@@ -419,7 +425,7 @@ class DeletedDeviceEntry:
             is_new=True,
         )
 
-    @cached_property
+    @under_cached_property
     def as_storage_fragment(self) -> json_fragment:
         """Return a json fragment for storage."""
         return json_fragment(
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index cf8b173edac..9d50b7ae83b 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -19,7 +19,6 @@ import time
 from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
 
 import attr
-from propcache import cached_property
 import voluptuous as vol
 
 from homeassistant.const import (
@@ -65,7 +64,12 @@ from .singleton import singleton
 from .typing import UNDEFINED, UndefinedType
 
 if TYPE_CHECKING:
+    # mypy cannot workout _cache Protocol with attrs
+    from propcache import cached_property as under_cached_property
+
     from homeassistant.config_entries import ConfigEntry
+else:
+    from propcache import under_cached_property
 
 DATA_REGISTRY: HassKey[EntityRegistry] = HassKey("entity_registry")
 EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = EventType(
@@ -162,7 +166,7 @@ def _protect_entity_options(
     return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()})
 
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, slots=True)
 class RegistryEntry:
     """Entity Registry Entry."""
 
@@ -201,6 +205,7 @@ class RegistryEntry:
     supported_features: int = attr.ib(default=0)
     translation_key: str | None = attr.ib(default=None)
     unit_of_measurement: str | None = attr.ib(default=None)
+    _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
 
     @domain.default
     def _domain_default(self) -> str:
@@ -247,7 +252,7 @@ class RegistryEntry:
                 display_dict["dp"] = precision
         return display_dict
 
-    @cached_property
+    @under_cached_property
     def display_json_repr(self) -> bytes | None:
         """Return a cached partial JSON representation of the entry.
 
@@ -267,7 +272,7 @@ class RegistryEntry:
             return None
         return json_repr
 
-    @cached_property
+    @under_cached_property
     def as_partial_dict(self) -> dict[str, Any]:
         """Return a partial dict representation of the entry."""
         # Convert sets and tuples to lists
@@ -296,7 +301,7 @@ class RegistryEntry:
             "unique_id": self.unique_id,
         }
 
-    @cached_property
+    @under_cached_property
     def extended_dict(self) -> dict[str, Any]:
         """Return a extended dict representation of the entry."""
         # Convert sets and tuples to lists
@@ -311,7 +316,7 @@ class RegistryEntry:
             "original_icon": self.original_icon,
         }
 
-    @cached_property
+    @under_cached_property
     def partial_json_repr(self) -> bytes | None:
         """Return a cached partial JSON representation of the entry."""
         try:
@@ -327,7 +332,7 @@ class RegistryEntry:
             )
         return None
 
-    @cached_property
+    @under_cached_property
     def as_storage_fragment(self) -> json_fragment:
         """Return a json fragment for storage."""
         return json_fragment(
@@ -394,7 +399,7 @@ class RegistryEntry:
         hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
 
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, slots=True)
 class DeletedRegistryEntry:
     """Deleted Entity Registry Entry."""
 
@@ -407,13 +412,14 @@ class DeletedRegistryEntry:
     orphaned_timestamp: float | None = attr.ib()
     created_at: datetime = attr.ib(factory=utcnow)
     modified_at: datetime = attr.ib(factory=utcnow)
+    _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
 
     @domain.default
     def _domain_default(self) -> str:
         """Compute domain value."""
         return split_entity_id(self.entity_id)[0]
 
-    @cached_property
+    @under_cached_property
     def as_storage_fragment(self) -> json_fragment:
         """Return a json fragment for storage."""
         return json_fragment(
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index ab8dfcf756f..94625746b05 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -27,7 +27,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration, mock_pla
 from tests.typing import WebSocketGenerator
 
 
-@attr.s(frozen=True)
+@attr.s(frozen=True, slots=True)
 class MockDeviceEntry(dr.DeviceEntry):
     """Device Registry Entry with fixed UUID."""
 
diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py
index 2a017b8d592..f74e8ea994e 100644
--- a/tests/components/homekit_controller/test_init.py
+++ b/tests/components/homekit_controller/test_init.py
@@ -289,6 +289,7 @@ async def test_snapshots(
             entry.pop("device_id", None)
             entry.pop("created_at", None)
             entry.pop("modified_at", None)
+            entry.pop("_cache", None)
 
             entities.append({"entry": entry, "state": state_dict})
 
@@ -297,6 +298,8 @@ async def test_snapshots(
         device_dict.pop("via_device_id", None)
         device_dict.pop("created_at", None)
         device_dict.pop("modified_at", None)
+        device_dict.pop("_cache", None)
+
         devices.append({"device": device_dict, "entities": entities})
 
     assert snapshot == devices
diff --git a/tests/syrupy.py b/tests/syrupy.py
index 0bdbcf99e2b..b6f753e6c7f 100644
--- a/tests/syrupy.py
+++ b/tests/syrupy.py
@@ -132,6 +132,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
         """Prepare a Home Assistant area registry entry for serialization."""
         serialized = AreaRegistryEntrySnapshot(dataclasses.asdict(data) | {"id": ANY})
         serialized.pop("_json_repr")
+        serialized.pop("_cache")
         return serialized
 
     @classmethod
@@ -156,6 +157,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
             serialized["via_device_id"] = ANY
         if serialized["primary_config_entry"] is not None:
             serialized["primary_config_entry"] = ANY
+        serialized.pop("_cache")
         return cls._remove_created_and_modified_at(serialized)
 
     @classmethod
@@ -182,6 +184,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
             }
         )
         serialized.pop("categories")
+        serialized.pop("_cache")
         return cls._remove_created_and_modified_at(serialized)
 
     @classmethod
-- 
GitLab