This is a tiny library to add "async generators" to Python 3.5. What are those?
Option 1: my 5-minute lightning talk demo from PyCon 2016
Option 2: read on!
Python's iterators are great to use -- but manually implementing the
iterator protocol (__iter__
, __next__
) can be very
annoying. No-one wants to do that all the time.
Fortunately, Python has generators, which make it easy and straightforward to create an iterator by writing a function. E.g., if you have a file where each line is a JSON document, you can make an iterator over the decoded bodies with:
def load_json_lines(fileobj):
for line in fileobj:
yield json.loads(line)
Starting in v3.5, Python has added *async iterators* and *async functions*. These are like regular iterators and functions, except that they have magic powers that let them do asynchronous I/O without twisting your control flow into knots.
Asynchronous I/O code is all about incrementally processing streaming
data, so async iterators are super handy. But manually implementing
the async iterator protocol (__aiter__
, __anext__
) can be very
annoying, which is why we want async generators, which make it easy
to create an async iterator by writing an async function. For
example, suppose that in our example above, we want to read the
documents from a network connection, instead of the local
filesystem. Using the asyncio.StreamReader interface
we can write:
async def load_json_lines(asyncio_stream_reader):
async for line in asyncio_stream_reader:
yield json.loads(line)
BUT! the above DOESN'T WORK in Python 3.5 -- you just get a syntax
error. In 3.5, the only way to make an async generator is to manually
define __aiter__
and __anext__
.
Until now.
This is a little library which implements async generators in Python
3.5, by emulating the above syntax. The two changes are that you have
to decorate your async generators with @async_generator
, and
instead of writing yield x
you write await yield_(x)
:
# Same example as before, but works in Python 3.5
from async_generator import async_generator, yield_, yield_from_
@async_generator
async def load_json_lines(asyncio_stream_reader):
async for line in asyncio_stream_reader:
await yield_(json.loads(line))
This library generally tries hard to match the semantics of Python
3.6's native async generators in every detail (PEP 525), except that it adds
yield from
support, and it doesn't currently support the
sys.{get,set}_asyncgen_hooks
garbage collection API. There are two
main reasons for this: (a) it doesn't exist on Python 3.5, and (b)
even on 3.6, only built-in generators are supposed to use that API,
and that's not us. In any case, you probably shouldn't be relying on
garbage collection for async generators – see this discussion
and PEP 533 for more
details.
As discussed above, you should always explicitly call aclose
on
async generators. To make this more convenient, this library also
includes an aclosing
async context manager. It acts just like the
closing
context manager included in the stdlib contextlib
module, but does await obj.aclose()
instead of
obj.close()
. Use it like this:
from async_generator import aclosing
async with aclosing(load_json_lines(asyncio_stream_reader)) as agen:
async for json_obj in agen:
...
Starting in 3.6, CPython has native support for async generators. But,
native async generators still don't support yield from
. This
library does. It looks like:
@async_generator
async def wrap_load_json_lines(asyncio_stream_reader):
await yield_from_(load_json_lines(asyncio_stream_reader))
The await yield_from_(...)
construction can be applied to any
async iterator, including class-based iterators, native async
generators, and async generators created using this library, and fully
supports the classic yield from
semantics.
For introspection purposes, we also export the following functions:
async_generator.isasyncgen
: Returns true if passed either an async generator object created by this library, or a native Python 3.6+ async generator object. Analogous toinspect.isasyncgen
in 3.6+.async_generator.isasyncgenfunction
: Returns true if passed either an async generator function created by this library, or a native Python 3.6+ async generator function. Analogous toinspect.isasyncgenfunction
in 3.6+.
Example:
>>> isasyncgenfunction(load_json_lines)
True
>>> gen_object = load_json_lines(asyncio_stream_reader)
>>> isasyncgen(gen_object)
True
In addition, this library's async generator objects are registered
with the collections.abc.AsyncGenerator
abstract base class:
>>> isinstance(gen_object, collections.abc.AsyncGenerator)
True
- Implement PEP 479: if a
StopAsyncIteration
leaks out of an async generator body, wrap it into aRuntimeError
. - If an async generator was instantiated but never iterated, then we used to issue a spurious "RuntimeWarning: coroutine '...' was never awaited" warning. This is now fixed.
- Add PyPy3 to our test matrix.
- 100% test coverage.
- Fix a subtle bug where if you wrapped an async generator using
functools.wraps
, thenisasyncgenfunction
would return True for the wrapper. This isn't howinspect.isasyncgenfunction
works, and it brokesphinxcontrib_trio
.
- Add support for async generator introspection attributes
ag_running
,ag_code
,ag_frame
. - Attempting to re-enter a running async_generator now raises
ValueError
, just like for native async generators. - 100% test coverage.
Remove (temporarily?) the hacks that let
yield_
andyield_from_
work with native async generators. It turns out that due to obscure linking issues this was causing the library to be entirely broken on Python 3.6 on Windows (but not Linux or MacOS). It's probably fixable, but needs some fiddling with ctypes to get the refcounting right, and I couldn't figure it out in the time I had available to spend.So in this version, everything that worked before still works with
@async_generator
-style generators, but uniformly, on all platforms,yield_
andyield_from_
now do not work inside native-style async generators.Now running CI testing on Windows as well as Linux.
100% test coverage.
- Allow
await yield_()
as an shorthand forawait yield_(None)
(thanks to Alex Grönholm for the suggestion+patch). - Small cleanups to setup.py and test infrastructure.
- 100% test coverage (now including branch coverage!)
- Added
isasyncgen
andisasyncgenfunction
. - On 3.6+, register our async generators with
collections.abc.AsyncGenerator
. - 100% test coverage.
- Rewrote
yield from
support; now has much more accurate handling of edge cases. yield_from_
now works inside CPython 3.6's native async generators.- Added
aclosing
context manager; it's pretty trivial, but if we're going to recommend it be used everywhere then it seems polite to include it. - 100% test coverage.
- Support for
asend
/athrow
/aclose
- Support for
yield from
- Add a
__del__
method that complains about improperly cleaned up async generators. - Adapt to the change in Python 3.5.2
where
__aiter__
should now be a regular method instead of an async method. - Adapt to Python 3.5.2's pickiness about iterating over already-exhausted coroutines.
- 100% test coverage.
- Fixes a very nasty and hard-to-hit bug where
await yield_(...)
calls could escape out to the top-level coroutine runner and get lost, if the last trap out to the coroutine runner before theawait yield_(...)
caused an exception to be injected. - Infinitesimally more efficient due to re-using internal
ANextIter
objects instead of recreating them on each call to__anext__
. - 100% test coverage.
Initial release.