From 547f32818c8cfe13bbc69a5779b288e6b93048b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Sep 2023 19:33:25 -0500 Subject: [PATCH] Make core States use cached_property (#100312) Need to validate this is worth removing __slots__ --- homeassistant/components/api/__init__.py | 6 ++-- .../components/websocket_api/commands.py | 8 ++--- .../components/websocket_api/messages.py | 2 +- homeassistant/core.py | 30 ++++--------------- homeassistant/helpers/template.py | 2 -- tests/test_core.py | 20 ++++++------- 6 files changed, 24 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index a1a2d1107b9..0cade0f81ca 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -202,11 +202,11 @@ class APIStatesView(HomeAssistantView): user: User = request["hass_user"] hass: HomeAssistant = request.app["hass"] if user.is_admin: - states = (state.as_dict_json() for state in hass.states.async_all()) + states = (state.as_dict_json for state in hass.states.async_all()) else: entity_perm = user.permissions.check_entity states = ( - state.as_dict_json() + state.as_dict_json for state in hass.states.async_all() if entity_perm(state.entity_id, "read") ) @@ -233,7 +233,7 @@ class APIEntityStateView(HomeAssistantView): if state := hass.states.get(entity_id): return web.Response( - body=state.as_dict_json(), + body=state.as_dict_json, content_type=CONTENT_TYPE_JSON, ) return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bd7d3b530cd..cef9e7bb706 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -270,7 +270,7 @@ def handle_get_states( states = _async_get_allowed_states(hass, connection) try: - serialized_states = [state.as_dict_json() for state in states] + serialized_states = [state.as_dict_json for state in states] except (ValueError, TypeError): pass else: @@ -281,7 +281,7 @@ def handle_get_states( serialized_states = [] for state in states: try: - serialized_states.append(state.as_dict_json()) + serialized_states.append(state.as_dict_json) except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", @@ -358,7 +358,7 @@ def handle_subscribe_entities( # to succeed for the UI to show. try: serialized_states = [ - state.as_compressed_state_json() + state.as_compressed_state_json for state in states if not entity_ids or state.entity_id in entity_ids ] @@ -371,7 +371,7 @@ def handle_subscribe_entities( serialized_states = [] for state in states: try: - serialized_states.append(state.as_compressed_state_json()) + serialized_states.append(state.as_compressed_state_json) except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 1114eec4fac..6e88c36c328 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -141,7 +141,7 @@ def _state_diff_event(event: Event) -> dict: if (event_old_state := event.data["old_state"]) is None: return { ENTITY_EVENT_ADD: { - event_new_state.entity_id: event_new_state.as_compressed_state() + event_new_state.entity_id: event_new_state.as_compressed_state } } if TYPE_CHECKING: diff --git a/homeassistant/core.py b/homeassistant/core.py index cbfc8097c7f..a43fa1997c6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -35,6 +35,7 @@ import voluptuous as vol import yarl from . import block_async_io, util +from .backports.functools import cached_property from .const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, @@ -1239,20 +1240,6 @@ class State: object_id: Object id of this state. """ - __slots__ = ( - "entity_id", - "state", - "attributes", - "last_changed", - "last_updated", - "context", - "domain", - "object_id", - "_as_dict", - "_as_dict_json", - "_as_compressed_state_json", - ) - def __init__( self, entity_id: str, @@ -1282,8 +1269,6 @@ class State: self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None - self._as_dict_json: str | None = None - self._as_compressed_state_json: str | None = None @property def name(self) -> str: @@ -1318,12 +1303,12 @@ class State: ) return self._as_dict + @cached_property def as_dict_json(self) -> str: """Return a JSON string of the State.""" - if not self._as_dict_json: - self._as_dict_json = json_dumps(self.as_dict()) - return self._as_dict_json + return json_dumps(self.as_dict()) + @cached_property def as_compressed_state(self) -> dict[str, Any]: """Build a compressed dict of a state for adds. @@ -1348,6 +1333,7 @@ class State: ) return compressed_state + @cached_property def as_compressed_state_json(self) -> str: """Build a compressed JSON key value pair of a state for adds. @@ -1355,11 +1341,7 @@ class State: It is used for sending multiple states in a single message. """ - if not self._as_compressed_state_json: - self._as_compressed_state_json = json_dumps( - {self.entity_id: self.as_compressed_state()} - )[1:-1] - return self._as_compressed_state_json + return json_dumps({self.entity_id: self.as_compressed_state})[1:-1] @classmethod def from_dict(cls, json_dict: dict[str, Any]) -> Self | None: diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 070e5b6d9ad..b0754c13c7c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -929,8 +929,6 @@ class DomainStates: class TemplateStateBase(State): """Class to represent a state object in a template.""" - __slots__ = ("_hass", "_collect", "_entity_id", "__dict__") - _state: State __setitem__ = _readonly diff --git a/tests/test_core.py b/tests/test_core.py index c5ce9eb0881..7cafadb638c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -671,11 +671,11 @@ def test_state_as_dict_json() -> None: '"last_changed":"1984-12-08T12:00:00","last_updated":"1984-12-08T12:00:00",' '"context":{"id":"01H0D6K3RFJAYAV2093ZW30PCW","parent_id":null,"user_id":null}}' ) - as_dict_json_1 = state.as_dict_json() + as_dict_json_1 = state.as_dict_json assert as_dict_json_1 == expected # 2nd time to verify cache - assert state.as_dict_json() == expected - assert state.as_dict_json() is as_dict_json_1 + assert state.as_dict_json == expected + assert state.as_dict_json is as_dict_json_1 def test_state_as_compressed_state() -> None: @@ -694,12 +694,12 @@ def test_state_as_compressed_state() -> None: "lc": last_time.timestamp(), "s": "on", } - as_compressed_state = state.as_compressed_state() + as_compressed_state = state.as_compressed_state # We are not too concerned about these being ReadOnlyDict # since we don't expect them to be called by external callers assert as_compressed_state == expected # 2nd time to verify cache - assert state.as_compressed_state() == expected + assert state.as_compressed_state == expected def test_state_as_compressed_state_unique_last_updated() -> None: @@ -720,12 +720,12 @@ def test_state_as_compressed_state_unique_last_updated() -> None: "lu": last_updated.timestamp(), "s": "on", } - as_compressed_state = state.as_compressed_state() + as_compressed_state = state.as_compressed_state # We are not too concerned about these being ReadOnlyDict # since we don't expect them to be called by external callers assert as_compressed_state == expected # 2nd time to verify cache - assert state.as_compressed_state() == expected + assert state.as_compressed_state == expected def test_state_as_compressed_state_json() -> None: @@ -740,13 +740,13 @@ def test_state_as_compressed_state_json() -> None: context=ha.Context(id="01H0D6H5K3SZJ3XGDHED1TJ79N"), ) expected = '"happy.happy":{"s":"on","a":{"pig":"dog"},"c":"01H0D6H5K3SZJ3XGDHED1TJ79N","lc":471355200.0}' - as_compressed_state = state.as_compressed_state_json() + as_compressed_state = state.as_compressed_state_json # We are not too concerned about these being ReadOnlyDict # since we don't expect them to be called by external callers assert as_compressed_state == expected # 2nd time to verify cache - assert state.as_compressed_state_json() == expected - assert state.as_compressed_state_json() is as_compressed_state + assert state.as_compressed_state_json == expected + assert state.as_compressed_state_json is as_compressed_state async def test_eventbus_add_remove_listener(hass: HomeAssistant) -> None: