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:
parent
1c19c54e38
commit
660c95d784
2 changed files with 54 additions and 18 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue