Skip to content

Commit

Permalink
feat: define overridable methods for each property (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
BobTheBuidler authored Feb 26, 2023
1 parent da1c378 commit 7a1004c
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 24 deletions.
30 changes: 17 additions & 13 deletions a_sync/_bound.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions a_sync/_descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

from async_property.base import AsyncPropertyDescriptor
from async_property.cached import AsyncCachedPropertyDescriptor
15 changes: 4 additions & 11 deletions a_sync/_meta.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
16 changes: 16 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

0 comments on commit 7a1004c

Please sign in to comment.