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
This commit is contained in:
J. Nick Koston 2023-07-20 02:59:17 -05:00 committed by GitHub
parent 1c19c54e38
commit 660c95d784
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 54 additions and 18 deletions

View file

@ -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: