From 6056753a9c116f8ef5f24243cfb310ebaff916d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 16:47:14 -0700 Subject: [PATCH] Introduce a singleton decorator (#34803) --- homeassistant/core.py | 2 +- homeassistant/helpers/device_registry.py | 28 ++++----------- homeassistant/helpers/entity_registry.py | 28 ++++----------- homeassistant/helpers/singleton.py | 44 ++++++++++++++++++++++++ pylintrc | 2 +- 5 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 homeassistant/helpers/singleton.py diff --git a/homeassistant/core.py b/homeassistant/core.py index 045e56ecb53..4d11d970c53 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -83,8 +83,8 @@ if TYPE_CHECKING: block_async_io.enable() fix_threading_exception_logging() -# pylint: disable=invalid-name T = TypeVar("T") +# pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ef85ac953f6..56b8170b99a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,8 +1,7 @@ """Provide a way to connect entities belonging to one device.""" -from asyncio import Event from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional import uuid import attr @@ -10,6 +9,7 @@ import attr from homeassistant.core import callback from homeassistant.loader import bind_hass +from .singleton import singleton from .typing import HomeAssistantType # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -356,26 +356,12 @@ class DeviceRegistry: @bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: - """Return device registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() - - reg = DeviceRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, Event): - evt = reg_or_evt - await evt.wait() - return cast(DeviceRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(DeviceRegistry, reg_or_evt) + """Create entity registry.""" + reg = DeviceRegistry(hass) + await reg.async_load() + return reg @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 10de8564fca..f276cc49850 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,7 +7,6 @@ The Entity Registry will persist itself 10 seconds after a new entity is registered. Registering a new entity while a timer is in progress resets the timer. """ -import asyncio from collections import OrderedDict import logging from typing import ( @@ -39,6 +38,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml +from .singleton import singleton from .typing import HomeAssistantType if TYPE_CHECKING: @@ -492,26 +492,12 @@ class EntityRegistry: @bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: - """Return entity registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = asyncio.Event() - - reg = EntityRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, asyncio.Event): - evt = reg_or_evt - await evt.wait() - return cast(EntityRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(EntityRegistry, reg_or_evt) + """Create entity registry.""" + reg = EntityRegistry(hass) + await reg.async_load() + return reg @callback @@ -621,4 +607,4 @@ async def async_migrate_entries( updates = entry_callback(entry) if updates is not None: - ent_reg.async_update_entity(entry.entity_id, **updates) # type: ignore + ent_reg.async_update_entity(entry.entity_id, **updates) diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py new file mode 100644 index 00000000000..8d0f6873c69 --- /dev/null +++ b/homeassistant/helpers/singleton.py @@ -0,0 +1,44 @@ +"""Helper to help coordinating calls.""" +import asyncio +import functools +from typing import Awaitable, Callable, TypeVar, cast + +from homeassistant.core import HomeAssistant + +T = TypeVar("T") + +FUNC = Callable[[HomeAssistant], Awaitable[T]] + + +def singleton(data_key: str) -> Callable[[FUNC], FUNC]: + """Decorate a function that should be called once per instance. + + Result will be cached and simultaneous calls will be handled. + """ + + def wrapper(func: FUNC) -> FUNC: + """Wrap a function with caching logic.""" + + @functools.wraps(func) + async def wrapped(hass: HomeAssistant) -> T: + obj_or_evt = hass.data.get(data_key) + + if not obj_or_evt: + evt = hass.data[data_key] = asyncio.Event() + + result = await func(hass) + + hass.data[data_key] = result + evt.set() + return cast(T, result) + + if isinstance(obj_or_evt, asyncio.Event): + evt = obj_or_evt + await evt.wait() + return cast(T, hass.data.get(data_key)) + + return cast(T, obj_or_evt) + + return wrapped + + return wrapper diff --git a/pylintrc b/pylintrc index a8118d23910..dd763d05886 100644 --- a/pylintrc +++ b/pylintrc @@ -8,7 +8,7 @@ persistent=no extension-pkg-whitelist=ciso8601 [BASIC] -good-names=id,i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp,T [MESSAGES CONTROL] # Reasons disabled: