Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: awaitable ASyncIterable and ASyncIterator #215

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions a_sync/iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,37 @@
import functools
import inspect
import logging

from async_property import async_cached_property

from a_sync import _helpers
from a_sync._typing import *


logger = logging.getLogger(__name__)

class ASyncIterable(AsyncIterable[T], Iterable[T]):
class _AwaitableAsyncIterableMixin(AsyncIterable[T]):
"""
A mixin class defining logic for awaiting an AsyncIterable
"""
__wrapped__: AsyncIterable[T]
def __aiter__(self) -> AsyncIterator[T]:
"Returns an async iterator for the wrapped async iterable."
return self.__wrapped__.__aiter__()
def __await__(self) -> Generator[Any, Any, List[T]]:
"""Asynchronously iterates through all contents of ``Self`` and returns a ``list`` containing the results."""
return self._materialized.__await__()
@property
def materialized(self) -> List[T]:
"""Iterates through all contents of ``Self`` and returns a ``list`` containing the results."""
return _helpers._await(self._materialized)
@async_cached_property
async def _materialized(self) -> List[T]:
"""Asynchronously iterates through all contents of ``Self`` and returns a ``list`` containing the results."""
return [obj async for obj in self]
__slots__ = '__async_property__',

class ASyncIterable(_AwaitableAsyncIterableMixin[T], Iterable[T]):
"""
Description:
A hybrid Iterable/AsyncIterable implementation designed to offer dual compatibility with both synchronous and asynchronous iteration protocols. This class allows objects to be iterated over using either a standard `for` loop or an `async for` loop, making it versatile in scenarios where the mode of iteration (synchronous or asynchronous) needs to be flexible or is determined at runtime.
Expand All @@ -30,14 +55,11 @@ def __repr__(self) -> str:
def __iter__(self) -> Iterator[T]:
"Returns an iterator for the wrapped async iterable."
yield from ASyncIterator(self.__aiter__())
def __aiter__(self) -> AsyncIterator[T]:
"Returns an async iterator for the wrapped async iterable."
return self.__wrapped__.__aiter__()
__slots__ = "__wrapped__",

AsyncGenFunc = Callable[P, AsyncGenerator[T, None]]

class ASyncIterator(AsyncIterator[T], Iterator[T]):
class ASyncIterator(_AwaitableAsyncIterableMixin[T], Iterator[T]):
"""
Description:
A hybrid Iterator/AsyncIterator implementation that bridges the gap between synchronous and asynchronous iteration. This class provides a unified interface for iteration that can seamlessly operate in both synchronous (`for` loop) and asynchronous (`async for` loop) contexts. It allows the wrapping of asynchronous iterable objects or async generator functions, making them usable in synchronous code without explicitly managing event loops or asynchronous context switches.
Expand Down
6 changes: 6 additions & 0 deletions tests/test_iter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ def test_iterator_wrap():
def test_iterable_sync():
assert [i for i in ASyncIterable(async_gen())] == [0, 1, 2]
assert [i for i in ASyncIterable.wrap(async_gen())] == [0, 1, 2]
assert ASyncIterable.wrap(async_gen()).materialized == [0, 1, 2]

@pytest.mark.asyncio_cooperative
async def test_iterable_async():
assert [i async for i in ASyncIterable(async_gen())] == [0, 1, 2]
assert [i async for i in ASyncIterable.wrap(async_gen())] == [0, 1, 2]
assert await ASyncIterable.wrap(async_gen()) == [0, 1, 2]

def test_iterator_sync():
iterator = ASyncIterator.wrap(async_gen())
Expand All @@ -36,6 +38,7 @@ def test_iterator_sync():
else:
with pytest.raises(StopIteration):
next(iterator)
assert ASyncIterator.wrap(async_gen()).materialized == [0, 1, 2]

@pytest.mark.asyncio_cooperative
async def test_iterator_async():
Expand All @@ -46,6 +49,7 @@ async def test_iterator_async():
else:
with pytest.raises(StopAsyncIteration):
await iterator.__anext__()
assert await ASyncIterator.wrap(async_gen()) == [0, 1, 2]

generator_wrap = ASyncIterator.wrap(async_gen)

Expand All @@ -57,6 +61,7 @@ def test_generator_sync():
else:
with pytest.raises(StopIteration):
iterator.__next__()
assert generator_wrap().materialized == [0, 1, 2]

@pytest.mark.asyncio_cooperative
async def test_generator_async():
Expand All @@ -67,6 +72,7 @@ async def test_generator_async():
else:
with pytest.raises(StopAsyncIteration):
await iterator.__anext__()
assert await generator_wrap() == [0, 1, 2]

class TestGenerator:
@ASyncIterator.wrap
Expand Down
Loading