Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache entity properties that are never expected to change in the base class #95315

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions homeassistant/backports/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
from types import GenericAlias
from typing import Any, Generic, Self, TypeVar, overload

_T = TypeVar("_T")
bdraco marked this conversation as resolved.
Show resolved Hide resolved
_T_co = TypeVar("_T_co", covariant=True)


class cached_property(Generic[_T]):
class cached_property(Generic[_T_co]): # pylint: disable=invalid-name
"""Backport of Python 3.12's cached_property.

Includes https://github.com/python/cpython/pull/101890/files
"""

def __init__(self, func: Callable[[Any], _T]) -> None:
def __init__(self, func: Callable[[Any], _T_co]) -> None:
"""Initialize."""
self.func: Callable[[Any], _T] = func
self.func: Callable[[Any], _T_co] = func
self.attrname: str | None = None
self.__doc__ = func.__doc__

Expand All @@ -35,12 +35,12 @@ def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
...

@overload
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T:
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T_co:
...

def __get__(
self, instance: Any | None, owner: type[Any] | None = None
) -> _T | Self:
) -> _T_co | Self:
"""Get."""
if instance is None:
return self
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/abode/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def is_on(self) -> bool:
"""Return True if the binary sensor is on."""
return cast(bool, self._device.is_on)

@property
@property # type: ignore[override]
# We don't know if the class may be set late here
# so we need to override the property to disable the cache.
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of the binary sensor."""
if self._device.get_value("is_window") == "1":
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/binary_sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -197,7 +198,7 @@ def _default_to_device_class_name(self) -> bool:
"""
return self.device_class is not None

@property
@cached_property
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/button/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
Expand Down Expand Up @@ -96,7 +97,7 @@ def _default_to_device_class_name(self) -> bool:
"""
return self.device_class is not None

@property
@cached_property
def device_class(self) -> ButtonDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
SERVICE_CLOSE_COVER,
Expand Down Expand Up @@ -250,7 +251,7 @@ def current_cover_tilt_position(self) -> int | None:
"""
return self._attr_current_cover_tilt_position

@property
@cached_property
def device_class(self) -> CoverDeviceClass | None:
frenck marked this conversation as resolved.
Show resolved Hide resolved
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/date/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DATE
from homeassistant.core import HomeAssistant, ServiceCall
Expand Down Expand Up @@ -75,7 +76,7 @@ class DateEntity(Entity):
_attr_native_value: date | None
_attr_state: None = None

@property
@cached_property
@final
def device_class(self) -> None:
"""Return the device class for the entity."""
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/datetime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
Expand Down Expand Up @@ -86,7 +87,7 @@ class DateTimeEntity(Entity):
_attr_state: None = None
_attr_native_value: datetime | None

@property
@cached_property
@final
def device_class(self) -> None:
"""Return entity device class."""
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/dsmr/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,10 @@ def available(self) -> bool:
"""Entity is only available if there is a telegram."""
return self.telegram is not None

@property
@property # type: ignore[override]
# The device class can change at runtime from GAS to ENERGY
# when new data is received. This should be remembered and restored
# at startup, but the integration currently doesn't support that.
def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of this entity."""
device_class = super().device_class
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
from typing import Any, Self, final

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import ( # noqa: F401
Expand Down Expand Up @@ -114,7 +115,7 @@ class EventEntity(RestoreEntity):
__last_event_type: str | None = None
__last_event_attributes: dict[str, Any] | None = None

@property
@cached_property
def device_class(self) -> EventDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
11 changes: 9 additions & 2 deletions homeassistant/components/filter/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,17 @@ def __init__(
self._state: StateType = None
self._filters = filters
self._attr_icon = None
self._attr_device_class = None
self._device_class = None
self._attr_state_class = None
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id}

@property
# This property is not cached because the underlying source may
# not always be available.
def device_class(self) -> SensorDeviceClass | None: # type: ignore[override]
"""Return the device class of the sensor."""
return self._device_class

@callback
def _update_filter_sensor_state_event(
self, event: EventType[EventStateChangedData]
Expand Down Expand Up @@ -283,7 +290,7 @@ def _update_filter_sensor_state(
self._state = temp_state.state

self._attr_icon = new_state.attributes.get(ATTR_ICON, ICON)
self._attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS)
self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS)
self._attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS)

if self._attr_native_unit_of_measurement != new_state.attributes.get(
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/group/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA,
DOMAIN as BINARY_SENSOR_DOMAIN,
Expand Down Expand Up @@ -147,7 +148,7 @@ def async_update_group_state(self) -> None:
# Set as ON if any / all member is ON
self._attr_is_on = self.mode(state == STATE_ON for state in states)

@property
@cached_property
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the sensor class of the binary sensor."""
return self._device_class
5 changes: 4 additions & 1 deletion homeassistant/components/group/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,10 @@ def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the sensor."""
return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute}

@property
@property # type: ignore[override]
# Because the device class is calculated, there is no guarantee that the
# sensors will be available when the entity is created so we do not want to
# cache the value.
def device_class(self) -> SensorDeviceClass | None:
"""Return device class."""
if self._attr_device_class is not None:
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/here_travel_time/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ def _handle_coordinator_update(self) -> None:
)
self.async_write_ha_state()

@property
@property # type: ignore[override]
# This property is not cached because the attribute can change
# at run time. This is not expected, but it is currently how
# the HERE integration works.
def attribution(self) -> str | None:
"""Return the attribution."""
if self.coordinator.data is not None:
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/huawei_lte/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,9 @@ def icon(self) -> str | None:
return self.entity_description.icon_fn(self.state)
return self.entity_description.icon

@property
@property # type: ignore[override]
# The device class might change at run time of the signal
# is not a number, so we override here.
def device_class(self) -> SensorDeviceClass | None:
"""Return device class for sensor."""
if self.entity_description.device_class_fn:
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/humidifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_MODE,
Expand Down Expand Up @@ -158,7 +159,7 @@ def capability_attributes(self) -> dict[str, Any]:

return data

@property
@cached_property
def device_class(self) -> HumidifierDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/image_processing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.components.camera import Image
from homeassistant.const import (
ATTR_ENTITY_ID,
Expand Down Expand Up @@ -156,7 +157,7 @@ def confidence(self) -> float | None:
return self.entity_description.confidence
return None

@property
@cached_property
def device_class(self) -> ImageProcessingDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
12 changes: 10 additions & 2 deletions homeassistant/components/integration/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ def __init__(
self._source_entity: str = source_entity
self._last_valid_state: Decimal | None = None
self._attr_device_info = device_info
self._device_class: SensorDeviceClass | None = None

@property # type: ignore[override]
# The underlying source data may be unavailable at startup, so the device
# class may be set late so we need to override the property to disable the cache.
def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of the sensor."""
return self._device_class

def _unit(self, source_unit: str) -> str:
"""Derive unit from the source sensor, SI prefix and time unit."""
Expand Down Expand Up @@ -288,7 +296,7 @@ async def async_added_to_hass(self) -> None:
err,
)

self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

@callback
Expand Down Expand Up @@ -319,7 +327,7 @@ def calc_integration(event: EventType[EventStateChangedData]) -> None:
and new_state.attributes.get(ATTR_DEVICE_CLASS)
== SensorDeviceClass.POWER
):
self._attr_device_class = SensorDeviceClass.ENERGY
self._device_class = SensorDeviceClass.ENERGY
self._attr_icon = None

self.async_write_ha_state()
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/media_player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import voluptuous as vol
from yarl import URL

from homeassistant.backports.functools import cached_property
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR
Expand Down Expand Up @@ -495,7 +496,7 @@ class MediaPlayerEntity(Entity):
_attr_volume_level: float | None = None

# Implement these for your media player
@property
@cached_property
def device_class(self) -> MediaPlayerDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/mobile_app/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def handle_sensor_registration(data):
)


class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): # type: ignore[misc]
"""Representation of an mobile app binary sensor."""

@property
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/mobile_app/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def entity_registry_enabled_default(self) -> bool:
"""Return if entity should be enabled by default."""
return not self._config.get(ATTR_SENSOR_DISABLED)

@property
@property # type: ignore[override,unused-ignore]
# Because the device class is received later from the mobile app
# we do not want to cache the property
def device_class(self):
"""Return the device class."""
return self._config.get(ATTR_SENSOR_DEVICE_CLASS)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/mobile_app/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def handle_sensor_registration(data):
)


class MobileAppSensor(MobileAppEntity, RestoreSensor):
class MobileAppSensor(MobileAppEntity, RestoreSensor): # type: ignore[misc]
"""Representation of an mobile app sensor."""

async def async_restore_last_state(self, last_state):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/number/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import voluptuous as vol

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature
from homeassistant.core import HomeAssistant, ServiceCall, callback
Expand Down Expand Up @@ -231,7 +232,7 @@ def _default_to_device_class_name(self) -> bool:
"""
return self.device_class is not None

@property
@cached_property
def device_class(self) -> NumberDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from math import ceil, floor, isfinite, log10
from typing import Any, Final, Self, cast, final

from homeassistant.backports.functools import cached_property
from homeassistant.config_entries import ConfigEntry

# pylint: disable-next=hass-deprecated-import
Expand Down Expand Up @@ -259,7 +260,7 @@ def _default_to_device_class_name(self) -> bool:
"""
return self.device_class not in (None, SensorDeviceClass.ENUM)

@property
@cached_property
def device_class(self) -> SensorDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
Expand Down
Loading