From fe84dffeb43fe0591ac854ca69ec414b27226dd0 Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 23 Apr 2024 01:04:01 +0000 Subject: [PATCH 1/2] feat: awaitable ASyncIterable and ASyncIterator --- a_sync/iter.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/a_sync/iter.py b/a_sync/iter.py index c0864bd2..389dac29 100644 --- a/a_sync/iter.py +++ b/a_sync/iter.py @@ -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. @@ -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. From 27da26f90d5671e880ae7aef71879d5a52ba86fa Mon Sep 17 00:00:00 2001 From: BobTheBuidler Date: Tue, 23 Apr 2024 00:46:32 +0000 Subject: [PATCH 2/2] feat(test): test iterable await --- tests/test_iter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_iter.py b/tests/test_iter.py index 08d73100..31735555 100644 --- a/tests/test_iter.py +++ b/tests/test_iter.py @@ -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()) @@ -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(): @@ -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) @@ -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(): @@ -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