From ab2ea6100ca8c165f7d695e1a90c0c57b46cb389 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 12:11:08 -0500 Subject: [PATCH] Speed up singleton decorator so it can be used more places (#116292) --- homeassistant/bootstrap.py | 10 ++++++---- homeassistant/helpers/entity.py | 10 +++++----- homeassistant/helpers/singleton.py | 1 + homeassistant/helpers/translation.py | 9 +++++---- homeassistant/requirements.py | 9 +++------ homeassistant/setup.py | 14 +++++--------- 6 files changed, 25 insertions(+), 28 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index cbc808eb0fa..8a77d438e84 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -90,7 +90,11 @@ from .helpers.system_info import async_get_system_info from .helpers.typing import ConfigType from .setup import ( BASE_PLATFORMS, - DATA_SETUP_STARTED, + # _setup_started is marked as protected to make it clear + # that it is not part of the public API and should not be used + # by integrations. It is only used for internal tracking of + # which integrations are being set up. + _setup_started, async_get_setup_timings, async_notify_setup_error, async_set_domains_to_be_loaded, @@ -913,9 +917,7 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: dict[str, Any] ) -> None: """Set up all the integrations.""" - setup_started: dict[tuple[str, str | None], float] = {} - hass.data[DATA_SETUP_STARTED] = setup_started - watcher = _WatchPendingSetups(hass, setup_started) + watcher = _WatchPendingSetups(hass, _setup_started(hass)) watcher.async_start() domains_to_setup, integration_cache = await _async_resolve_domains_to_setup( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 07d5410f3f2..cf493b5477e 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -66,7 +66,7 @@ from homeassistant.loader import async_suggest_report_issue, bind_hass from homeassistant.util import ensure_unique_string, slugify from homeassistant.util.frozen_dataclass_compat import FrozenOrThawed -from . import device_registry as dr, entity_registry as er +from . import device_registry as dr, entity_registry as er, singleton from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData from .event import ( async_track_device_registry_updated_event, @@ -98,15 +98,15 @@ CONTEXT_RECENT_TIME_SECONDS = 5 # Time that a context is considered recent @callback def async_setup(hass: HomeAssistant) -> None: """Set up entity sources.""" - hass.data[DATA_ENTITY_SOURCE] = {} + entity_sources(hass) @callback @bind_hass +@singleton.singleton(DATA_ENTITY_SOURCE) def entity_sources(hass: HomeAssistant) -> dict[str, EntityInfo]: """Get the entity sources.""" - _entity_sources: dict[str, EntityInfo] = hass.data[DATA_ENTITY_SOURCE] - return _entity_sources + return {} def generate_entity_id( @@ -1486,7 +1486,7 @@ class Entity( # The check for self.platform guards against integrations not using an # EntityComponent and can be removed in HA Core 2024.1 if self.platform: - self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id) + entity_sources(self.hass).pop(self.entity_id) @callback def _async_registry_updated( diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index 91e7a671b69..bf9b6019164 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -25,6 +25,7 @@ def singleton(data_key: str) -> Callable[[_FuncType[_T]], _FuncType[_T]]: """Wrap a function with caching logic.""" if not asyncio.iscoroutinefunction(func): + @functools.lru_cache(maxsize=1) @bind_hass @functools.wraps(func) def wrapped(hass: HomeAssistant) -> _T: diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 377826b7edb..182747ec415 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -24,6 +24,8 @@ from homeassistant.loader import ( ) from homeassistant.util.json import load_json +from . import singleton + _LOGGER = logging.getLogger(__name__) TRANSLATION_FLATTEN_CACHE = "translation_flatten_cache" @@ -370,11 +372,10 @@ def async_get_cached_translations( ) -@callback +@singleton.singleton(TRANSLATION_FLATTEN_CACHE) def _async_get_translations_cache(hass: HomeAssistant) -> _TranslationCache: """Return the translation cache.""" - cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE] - return cache + return _TranslationCache(hass) @callback @@ -385,7 +386,7 @@ def async_setup(hass: HomeAssistant) -> None: """ cache = _TranslationCache(hass) current_language = hass.config.language - hass.data[TRANSLATION_FLATTEN_CACHE] = cache + _async_get_translations_cache(hass) @callback def _async_load_translations_filter(event_data: Mapping[str, Any]) -> bool: diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index e282ced90ac..e29e0c34ece 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -12,6 +12,7 @@ from packaging.requirements import Requirement from .core import HomeAssistant, callback from .exceptions import HomeAssistantError +from .helpers import singleton from .helpers.typing import UNDEFINED, UndefinedType from .loader import Integration, IntegrationNotFound, async_get_integration from .util import package as pkg_util @@ -72,14 +73,10 @@ async def async_load_installed_versions( @callback +@singleton.singleton(DATA_REQUIREMENTS_MANAGER) def _async_get_manager(hass: HomeAssistant) -> RequirementsManager: """Get the requirements manager.""" - if DATA_REQUIREMENTS_MANAGER in hass.data: - manager: RequirementsManager = hass.data[DATA_REQUIREMENTS_MANAGER] - return manager - - manager = hass.data[DATA_REQUIREMENTS_MANAGER] = RequirementsManager(hass) - return manager + return RequirementsManager(hass) @callback diff --git a/homeassistant/setup.py b/homeassistant/setup.py index fab70e31d9d..894fc0eeb73 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -29,7 +29,7 @@ from .core import ( callback, ) from .exceptions import DependencyError, HomeAssistantError -from .helpers import translation +from .helpers import singleton, translation from .helpers.issue_registry import IssueSeverity, async_create_issue from .helpers.typing import ConfigType from .util.async_ import create_eager_task @@ -671,13 +671,12 @@ class SetupPhases(StrEnum): """Wait time for the packages to import.""" +@singleton.singleton(DATA_SETUP_STARTED) def _setup_started( hass: core.HomeAssistant, ) -> dict[tuple[str, str | None], float]: """Return the setup started dict.""" - if DATA_SETUP_STARTED not in hass.data: - hass.data[DATA_SETUP_STARTED] = {} - return hass.data[DATA_SETUP_STARTED] # type: ignore[no-any-return] + return {} @contextlib.contextmanager @@ -717,15 +716,12 @@ def async_pause_setup( ) +@singleton.singleton(DATA_SETUP_TIME) def _setup_times( hass: core.HomeAssistant, ) -> defaultdict[str, defaultdict[str | None, defaultdict[SetupPhases, float]]]: """Return the setup timings default dict.""" - if DATA_SETUP_TIME not in hass.data: - hass.data[DATA_SETUP_TIME] = defaultdict( - lambda: defaultdict(lambda: defaultdict(float)) - ) - return hass.data[DATA_SETUP_TIME] # type: ignore[no-any-return] + return defaultdict(lambda: defaultdict(lambda: defaultdict(float))) @contextlib.contextmanager