feat(helpers): implement new descriptor with default value getter

This commit is contained in:
mpa 2020-05-25 05:05:42 +04:00
parent 8e54cce58e
commit bb398ebb4f
3 changed files with 149 additions and 43 deletions

View file

@ -8,6 +8,7 @@ from typing import Any, AsyncGenerator, Callable, ClassVar, Optional, Type, Type
from aiogram.utils.exceptions import TelegramAPIError
from ....utils.helper import DefaultProperty
from ...methods import Response, TelegramMethod
from ..telegram import PRODUCTION, TelegramAPIServer
@ -20,47 +21,12 @@ class BaseSession(abc.ABC):
# global session timeout
default_timeout: ClassVar[float] = 60.0
_api: TelegramAPIServer
_json_loads: _JsonLoads
_json_dumps: _JsonDumps
_timeout: float
@property
def api(self) -> TelegramAPIServer:
return getattr(self, "_api", PRODUCTION) # type: ignore
@api.setter
def api(self, value: TelegramAPIServer) -> None:
self._api = value
@property
def json_loads(self) -> _JsonLoads:
return getattr(self, "_json_loads", json.loads) # type: ignore
@json_loads.setter
def json_loads(self, value: _JsonLoads) -> None:
self._json_loads = value # type: ignore
@property
def json_dumps(self) -> _JsonDumps:
return getattr(self, "_json_dumps", json.dumps) # type: ignore
@json_dumps.setter
def json_dumps(self, value: _JsonDumps) -> None:
self._json_dumps = value # type: ignore
@property
def timeout(self) -> float:
return getattr(self, "_timeout", self.__class__.default_timeout) # type: ignore
@timeout.setter
def timeout(self, value: float) -> None:
self._timeout = value
@timeout.deleter
def timeout(self) -> None:
if hasattr(self, "_timeout"):
del self._timeout
api: DefaultProperty[TelegramAPIServer] = DefaultProperty(PRODUCTION)
json_loads: DefaultProperty[_JsonLoads] = DefaultProperty(json.loads)
json_dumps: DefaultProperty[_JsonDumps] = DefaultProperty(json.dumps)
timeout: DefaultProperty[float] = DefaultProperty(
fget=lambda self: float(self.__class__.default_timeout)
)
@classmethod
def raise_for_status(cls, response: Response[T]) -> None:

View file

@ -14,7 +14,9 @@ Example:
<<< ['barItem', 'bazItem', 'fooItem', 'lorem']
"""
import inspect
from typing import Any, Callable, Iterable, List, Optional, Union, cast
from typing import Any, Callable, Generic, Iterable, List, Optional, TypeVar, Union, cast
T = TypeVar("T")
PROPS_KEYS_ATTR_NAME = "_props_keys"
@ -233,3 +235,60 @@ class OrderedHelper(Helper, metaclass=OrderedHelperMeta):
else:
result.append(value)
return result
class DefaultProperty(Generic[T]):
"""
Implements descriptor. Intended to be used as a class attribute and only internally.
"""
__slots__ = (
"name_resolver",
"name",
"fget",
)
def __init__(
self,
default: Optional[T] = None,
*,
name_resolver: Callable[[str], str] = lambda s: "_" + s,
fget: Optional[Callable[[Any], T]] = None,
) -> None:
if fget is None is default:
raise ValueError("Either default or fget should be passed.")
self.fget = fget or (lambda _: cast(T, default))
self.name_resolver = name_resolver
self.name: Optional[str] = None
def __set_name__(self, owner: Any, name: str) -> None:
self.name = self.name_resolver(name)
def _raise_if_name_never_set(self, instance: Any) -> None:
if self.name is None:
raise AttributeError(f"Name for descriptor was never set in {instance}")
def __get__(self, instance: Any, owner: Any) -> T:
if instance is None:
return self.fget(instance)
self._raise_if_name_never_set(instance)
return cast(T, getattr(instance, self.name, self.fget(instance))) # type: ignore
def __set__(self, instance: Any, value: T) -> None:
if instance is None or isinstance(instance, type):
raise AttributeError(
"Instance cannot be class or None. Setter must be called from a class."
)
self._raise_if_name_never_set(instance)
setattr(instance, self.name, value) # type: ignore
def __delete__(self, instance: Any) -> None:
if instance is None or isinstance(instance, type):
raise AttributeError(
"Instance cannot be class or None. Deleter must be called from a class."
)
self._raise_if_name_never_set(instance)
delattr(instance, self.name) # type: ignore

View file

@ -1,6 +1,6 @@
import pytest
from aiogram.utils.helper import Helper, HelperMode, Item, ListItem, OrderedHelper
from aiogram.utils.helper import DefaultProperty, Helper, HelperMode, Item, ListItem, OrderedHelper
class TestHelper:
@ -132,3 +132,84 @@ class TestOrderedHelper:
B = ListItem()
assert MyOrderedHelper.all() == ["A", "D", "C", "B"]
class TestDefaultDescriptor:
def test_descriptor_fs(self):
obj = type("ClassA", (), {})()
default_x_val = "some_x"
x = DefaultProperty(default_x_val)
x.__set_name__(owner=obj.__class__, name="x")
# we can omit owner, usually it's just obj.__class__
assert x.__get__(instance=obj, owner=None) == default_x_val
assert x.__get__(instance=obj, owner=obj.__class__) == default_x_val
new_x_val = "new_x"
assert x.__set__(instance=obj, value=new_x_val) is None
with pytest.raises(AttributeError) as exc:
x.__set__(instance=obj.__class__, value="will never be set")
assert "Instance cannot be class or None" in str(exc.value)
assert x.__get__(instance=obj, owner=obj.__class__) == new_x_val
with pytest.raises(AttributeError) as exc:
x.__delete__(instance=obj.__class__)
assert "Instance cannot be class or None" in str(exc.value)
x.__delete__(instance=obj)
assert x.__get__(instance=obj, owner=obj.__class__) == default_x_val
def test_set_name(self):
class A:
x = DefaultProperty("default")
a = A()
assert "_x" not in vars(a)
a.x = "new value"
assert "_x" in vars(a)
del a.x
assert "_x" not in vars(a)
assert a.x == "default"
class B:
x = DefaultProperty("default", name_resolver=lambda name: f"_{name}_4_{name}_")
b = B()
b.x = ">>"
assert "_x_4_x_" in vars(b)
C = type("C", (), {"x": DefaultProperty("default")})
c = C()
assert c.x == "default"
c.x = "new value"
assert "_x" in vars(c)
d = type("D", (), {})()
x = DefaultProperty("default")
with pytest.raises(AttributeError) as exc:
x.__set__(d, "new_value")
assert f"Name for descriptor was never set in {d}" in str(exc.value)
def test_init(self):
class A:
x = DefaultProperty(fget=lambda a_inst: "nothing")
a = A()
assert a.x == "nothing"
x = DefaultProperty("x")
assert x.__get__(None, None) == "x"
assert x.fget(None) == x.__get__(None, None)
with pytest.raises(ValueError) as exc:
class _:
x = DefaultProperty(default=None, fget=None)
assert "Either default or fget should be passed." in str(exc.value)