From 7a1004cec0b9fd61833aea23e60c046c2c884dd5 Mon Sep 17 00:00:00 2001 From: BobTheBuidler <70677534+BobTheBuidler@users.noreply.github.com> Date: Sun, 26 Feb 2023 03:16:27 -0500 Subject: [PATCH] feat: define overridable methods for each property (#10) --- a_sync/_bound.py | 30 +++++++++++++++++------------- a_sync/_descriptors.py | 3 +++ a_sync/_meta.py | 15 ++++----------- tests/test_base.py | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 a_sync/_descriptors.py diff --git a/a_sync/_bound.py b/a_sync/_bound.py index 4e325712..11967112 100644 --- a/a_sync/_bound.py +++ b/a_sync/_bound.py @@ -1,13 +1,11 @@ import functools from inspect import isawaitable -from typing import Any, Callable, TypeVar, Union, overload, Awaitable +from typing import Callable, TypeVar, overload, Awaitable -from async_property.base import AsyncPropertyDescriptor -from async_property.cached import AsyncCachedPropertyDescriptor from typing_extensions import ParamSpec # type: ignore -from a_sync import _helpers +from a_sync import _helpers, _descriptors from a_sync.decorator import a_sync as unbound_a_sync P = ParamSpec("P") @@ -37,23 +35,29 @@ def bound_a_sync_wrap(self, *args: P.args, **kwargs: P.kwargs) -> T: # type: ig @overload -def a_sync_property(async_property: AsyncPropertyDescriptor) -> AsyncPropertyDescriptor: +def a_sync_property(async_property: _descriptors.AsyncPropertyDescriptor) -> _descriptors.AsyncPropertyDescriptor: ... @overload -def a_sync_property(async_property: AsyncCachedPropertyDescriptor) -> AsyncCachedPropertyDescriptor: # type: ignore +def a_sync_property(async_property: _descriptors.AsyncCachedPropertyDescriptor) -> _descriptors.AsyncCachedPropertyDescriptor: # type: ignore ... -def a_sync_property(async_property): - if not isinstance(async_property, (AsyncPropertyDescriptor, AsyncCachedPropertyDescriptor)): +def a_sync_property(async_property) -> tuple: + if not isinstance(async_property, (_descriptors.AsyncPropertyDescriptor, _descriptors.AsyncCachedPropertyDescriptor)): raise TypeError(f"{async_property} must be one of: AsyncPropertyDescriptor, AsyncCachedPropertyDescriptor") - + from a_sync.base import ASyncBase from a_sync._meta import ASyncMeta - @property + async_property.hidden_method_name = f"__{async_property.field_name}" + @functools.wraps(async_property) - def a_sync_property_wrap(self) -> T: + def a_sync_method(self, **kwargs): if not isinstance(self, ASyncBase) and not isinstance(self.__class__, ASyncMeta): raise RuntimeError(f"{self} must be an instance of a class that eiher inherits from ASyncBase or specifies ASyncMeta as its metaclass.") awaitable: Awaitable[T] = async_property.__get__(self, async_property) - return _helpers._await_if_sync(awaitable, self._should_await({})) # type: ignore - return a_sync_property_wrap + return _helpers._await_if_sync(awaitable, self._should_await(kwargs)) # type: ignore + @property + @functools.wraps(async_property) + def a_sync_property(self) -> T: + a_sync_method = getattr(self, async_property.hidden_method_name) + return _helpers._await_if_sync(a_sync_method(sync=False), self._should_await({})) + return a_sync_property, a_sync_method diff --git a/a_sync/_descriptors.py b/a_sync/_descriptors.py new file mode 100644 index 00000000..210a9428 --- /dev/null +++ b/a_sync/_descriptors.py @@ -0,0 +1,3 @@ + +from async_property.base import AsyncPropertyDescriptor +from async_property.cached import AsyncCachedPropertyDescriptor diff --git a/a_sync/_meta.py b/a_sync/_meta.py index 8e3f5fce..d4645f1b 100644 --- a/a_sync/_meta.py +++ b/a_sync/_meta.py @@ -1,22 +1,15 @@ from asyncio import iscoroutinefunction -from async_property.base import AsyncPropertyDescriptor -from async_property.cached import AsyncCachedPropertyDescriptor - -from a_sync import _bound - +from a_sync import _bound, _descriptors class ASyncMeta(type): """Any class with metaclass ASyncMeta will have its functions wrapped with a_sync upon class instantiation.""" def __new__(cls, name, bases, attrs): - for attr_name, attr_value in attrs.items(): + for attr_name, attr_value in list(attrs.items()): # Special handling for functions decorated with async_property and async_cached_property - if isinstance(attr_value, (AsyncPropertyDescriptor, AsyncCachedPropertyDescriptor)): - print(attr_name) - print(attrs[attr_name]) - attrs[attr_name] = _bound.a_sync_property(attr_value) - print(attrs[attr_name]) + if isinstance(attr_value, (_descriptors.AsyncPropertyDescriptor, _descriptors.AsyncCachedPropertyDescriptor)): + attrs[attr_name], attrs[attr_value.hidden_method_name] = _bound.a_sync_property(attr_value) elif iscoroutinefunction(attr_value): # NOTE We will need to improve this logic if somebody needs to use it with classmethods or staticmethods. attrs[attr_name] = _bound.a_sync(attr_value) diff --git a/tests/test_base.py b/tests/test_base.py index 8abb77c9..ccf7987d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -24,6 +24,15 @@ def test_base_sync(i: int): val = asyncio.get_event_loop().run_until_complete(sync_instance.test_fn(sync=False)) assert isinstance(val, int) + # Can we access hidden methods for properties? + assert sync_instance.__test_property() == i * 2 + start = time.time() + assert sync_instance.__test_cached_property() == i * 3 + # Can we override them too? + assert asyncio.get_event_loop().run_until_complete(sync_instance.__test_cached_property(sync=False)) == i * 3 + duration = time.time() - start + assert duration < 3, "There is a 2 second sleep in 'test_cached_property' but it should only run once." + @pytest.mark.asyncio @pytest.mark.parametrize('i', list(range(10))) async def test_base_async(i: int): @@ -41,3 +50,10 @@ async def test_base_async(i: int): # Can we override with kwargs? with pytest.raises(RuntimeError): async_instance.test_fn(sync=True) + + # Can we access hidden methods for properties? + assert await async_instance.__test_property() == i * 2 + assert await async_instance.__test_cached_property() == i * 3 + # Can we override them too? + with pytest.raises(RuntimeError): + async_instance.__test_cached_property(sync=True)