Source code for python_utils.aio
"""Asyncio equivalents of common synchronous helpers.
These bring ``itertools``-style ergonomics to ``async for``: ``acount`` is an
async counter, while ``acontainer`` and ``adict`` collect an async iterable
into a concrete container.
"""
import asyncio
import collections.abc
import itertools
import typing
#: Numeric type (``int`` or ``float``) produced by ``acount``.
_N = typing.TypeVar('_N', int, float)
#: Element type of the async iterables being consumed.
_T = typing.TypeVar('_T')
#: Key type when collecting an async iterable of pairs into a dict.
_K = typing.TypeVar('_K')
#: Value type when collecting an async iterable of pairs into a dict.
_V = typing.TypeVar('_V')
[docs]
async def acount(
start: _N = 0,
step: _N = 1,
delay: float = 0,
stop: _N | None = None,
) -> collections.abc.AsyncIterator[_N]:
"""Async equivalent of ``itertools.count()``.
Counts from ``start`` in steps of ``step``, sleeping ``delay`` seconds
between values, and stops once ``stop`` (when given) is reached.
Args:
start: First value to yield.
step: Amount added between successive values.
delay: Seconds to ``asyncio.sleep`` between yields.
stop: Exclusive upper bound; ``None`` counts forever.
Yields:
The successive counter values.
>>> async def demo():
... return [i async for i in acount(stop=3)]
>>> asyncio.run(demo())
[0, 1, 2]
"""
for item in itertools.count(start, step): # pragma: no branch
if stop is not None and item >= stop:
break
yield item
await asyncio.sleep(delay)
@typing.overload
async def acontainer(
iterable: collections.abc.AsyncIterable[_T]
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
container: type[tuple[_T, ...]],
) -> tuple[_T, ...]:
"""Overload: collect the async iterable into a ``tuple``."""
@typing.overload
async def acontainer(
iterable: collections.abc.AsyncIterable[_T]
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
container: type[list[_T]] = list,
) -> list[_T]:
"""Overload: collect the async iterable into a ``list`` (the default)."""
@typing.overload
async def acontainer(
iterable: collections.abc.AsyncIterable[_T]
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
container: type[set[_T]],
) -> set[_T]:
"""Overload: collect the async iterable into a ``set``."""
[docs]
async def acontainer(
iterable: collections.abc.AsyncIterable[_T]
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
container: collections.abc.Callable[
[collections.abc.Iterable[_T]], collections.abc.Collection[_T]
] = list,
) -> collections.abc.Collection[_T]:
"""
Asyncio version of list()/set()/tuple()/etc() using an async for loop.
So instead of doing `[item async for item in iterable]` you can do
`await acontainer(iterable)`.
"""
iterable_: collections.abc.AsyncIterable[_T]
if callable(iterable):
iterable_ = iterable()
else:
iterable_ = iterable
items: list[_T] = [item async for item in iterable_]
return container(items)
[docs]
async def adict(
iterable: collections.abc.AsyncIterable[tuple[_K, _V]]
| collections.abc.Callable[
..., collections.abc.AsyncIterable[tuple[_K, _V]]
],
container: collections.abc.Callable[
[collections.abc.Iterable[tuple[_K, _V]]],
collections.abc.Mapping[_K, _V],
] = dict,
) -> collections.abc.Mapping[_K, _V]:
"""
Asyncio version of dict() using an async for loop.
So instead of doing `{key: value async for key, value in iterable}` you
can do `await adict(iterable)`.
"""
iterable_: collections.abc.AsyncIterable[tuple[_K, _V]]
if callable(iterable):
iterable_ = iterable()
else:
iterable_ = iterable
items: list[tuple[_K, _V]] = [item async for item in iterable_]
return container(items)