From 660c95d78409ebe828e8c231ab774e8f19d162cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Jul 2023 02:59:17 -0500 Subject: [PATCH] Pre-split unifiprotect nested attribute lookups (#96862) * Pre-split unifiprotect nested attribute lookups replaces and closes #96631 * Pre-split unifiprotect nested attribute lookups replaces and closes #96631 * comments --- .../components/unifiprotect/models.py | 64 +++++++++++++++---- .../components/unifiprotect/utils.py | 8 +-- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 375784d0323..c250a021340 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel @@ -19,6 +19,15 @@ _LOGGER = logging.getLogger(__name__) T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) +def split_tuple(value: tuple[str, ...] | str | None) -> tuple[str, ...] | None: + """Split string to tuple.""" + if value is None: + return None + if TYPE_CHECKING: + assert isinstance(value, str) + return tuple(value.split(".")) + + class PermRequired(int, Enum): """Type of permission level required for entity.""" @@ -31,18 +40,34 @@ class PermRequired(int, Enum): class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): """Mixin for required keys.""" - ufp_required_field: str | None = None - ufp_value: str | None = None + # `ufp_required_field`, `ufp_value`, and `ufp_enabled` are defined as + # a `str` in the dataclass, but `__post_init__` converts it to a + # `tuple[str, ...]` to avoid doing it at run time in `get_nested_attr` + # which is usually called millions of times per day. + ufp_required_field: tuple[str, ...] | str | None = None + ufp_value: tuple[str, ...] | str | None = None ufp_value_fn: Callable[[T], Any] | None = None - ufp_enabled: str | None = None + ufp_enabled: tuple[str, ...] | str | None = None ufp_perm: PermRequired | None = None + def __post_init__(self) -> None: + """Pre-convert strings to tuples for faster get_nested_attr.""" + self.ufp_required_field = split_tuple(self.ufp_required_field) + self.ufp_value = split_tuple(self.ufp_value) + self.ufp_enabled = split_tuple(self.ufp_enabled) + def get_ufp_value(self, obj: T) -> Any: """Return value from UniFi Protect device.""" - if self.ufp_value is not None: - return get_nested_attr(obj, self.ufp_value) - if self.ufp_value_fn is not None: - return self.ufp_value_fn(obj) + if (ufp_value := self.ufp_value) is not None: + if TYPE_CHECKING: + # `ufp_value` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_value, tuple) + return get_nested_attr(obj, ufp_value) + if (ufp_value_fn := self.ufp_value_fn) is not None: + return ufp_value_fn(obj) # reminder for future that one is required raise RuntimeError( # pragma: no cover @@ -51,16 +76,27 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): def get_ufp_enabled(self, obj: T) -> bool: """Return value from UniFi Protect device.""" - if self.ufp_enabled is not None: - return bool(get_nested_attr(obj, self.ufp_enabled)) + if (ufp_enabled := self.ufp_enabled) is not None: + if TYPE_CHECKING: + # `ufp_enabled` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_enabled, tuple) + return bool(get_nested_attr(obj, ufp_enabled)) return True def has_required(self, obj: T) -> bool: """Return if has required field.""" - - if self.ufp_required_field is None: + if (ufp_required_field := self.ufp_required_field) is None: return True - return bool(get_nested_attr(obj, self.ufp_required_field)) + if TYPE_CHECKING: + # `ufp_required_field` is defined as a `str` in the dataclass, but + # `__post_init__` converts it to a `tuple[str, ...]` to avoid + # doing it at run time in `get_nested_attr` which is usually called + # millions of times per day. This tells mypy that it's a tuple. + assert isinstance(ufp_required_field, tuple) + return bool(get_nested_attr(obj, ufp_required_field)) @dataclass @@ -73,7 +109,7 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): """Return value from UniFi Protect device.""" if self.ufp_event_obj is not None: - return cast(Event, get_nested_attr(obj, self.ufp_event_obj)) + return cast(Event, getattr(obj, self.ufp_event_obj, None)) return None def get_is_on(self, event: Event | None) -> bool: diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index e0c56cfd5fc..3e2b5e1b19e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -41,13 +41,13 @@ from .const import ( _SENTINEL = object() -def get_nested_attr(obj: Any, attr: str) -> Any: +def get_nested_attr(obj: Any, attrs: tuple[str, ...]) -> Any: """Fetch a nested attribute.""" - if "." not in attr: - value = getattr(obj, attr, None) + if len(attrs) == 1: + value = getattr(obj, attrs[0], None) else: value = obj - for key in attr.split("."): + for key in attrs: if (value := getattr(value, key, _SENTINEL)) is _SENTINEL: return None