4
\$\begingroup\$

Since Python 3.6 and PEP 525 one can use asynchronous generator:

import asyncio
async def asyncgen():
 yield 1
 yield 2
async def main():
 async for i in asyncgen():
 print(i)
asyncio.run(main())

I created a function which is able to wrap any asynchronous generator, the same way you would wrap a basic function using @decorator.

def asyncgen_wrapper(generator):
 async def wrapped(*args, **kwargs):
 print("The wrapped asynchronous generator is iterated")
 gen = generator(*args, **kwargs)
 try:
 value = await gen.__anext__()
 except StopAsyncIteration:
 return
 while True:
 to_send = yield value
 try:
 value = await gen.asend(to_send)
 except StopAsyncIteration:
 return
 return wrapped

Wrapping an asynchronous generator seems quite complicated compared to wrapping a basic generator:

def gen_wrapper(generator):
 def wrapped(*args, **kwargs):
 return (yield from generator(*args, **kwargs))
 return wrapped

I mainly concerned about correctness of my wrapper. I want the wrapper to be as transparent as possible regarding the wrapped generator. Which leads me to have two questions:

  • Is there a more straightforward way to implement a decorator for asynchronous generators?
  • Does my implementation handle all possible edge cases (think to asend() for example)?
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked May 6, 2020 at 17:09
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

"Correct" is nebulous. If it were to mean here something like "the semantics of any wrapped generator through the public API is the same as if it were not wrapped, apart from a print statement before the generator is created", then, no, the code isn't correct since it doesn't preserve the effects of raising an exception or closing the generator. For example,

import asyncio
class StopItNow( Exception ): 
 pass
async def f():
 try:
 yield 0
 except StopItNow:
 yield 1
async def g( f ): 
 x = f()
 print( await x.asend( None ) )
 print( await x.athrow( StopItNow ) )
asyncio.run( g( f ) )
asyncio.run( g( asyncgen_wrapper( f ) ) )

. I think it would be a non-trivial undertaking to "correctly" implement a wrapper. Interestingly, in the same PEP (525) that you've linked in your question,

While it is theoretically possible to implement yield from support for asynchronous generators, it would require a serious redesign of the generators implementation.

https://www.python.org/dev/peps/pep-0525/#asynchronous-yield-from

. You might find that it's a whole lot easier to implement some sort of injection or parameterise the generator itself to accept callables.

Otherwise, from my experimentation and taking hints from PEP 380, there are implementation details that are omitted from PEP 525 that would be necessary to emulate the behaviour of an unwrapped generator.

This is the result of some toying around:

import functools
import sys
def updated_asyncgen_wrapper( generator ): 
 @functools.wraps( generator )
 async def wrapped( *args, **kwargs ): 
 print( "iterating wrapped generator" )
 gen = generator( *args, **kwargs )
 to_send, is_exc = ( None, ), False
 while True:
 try:
 do = gen.athrow if is_exc else gen.asend
 value = await do( *to_send )
 except StopAsyncIteration:
 return
 try:
 to_send, is_exc = ( (yield value), ), False
 except GeneratorExit:
 await gen.aclose()
 raise
 except:
 to_send, is_exc = sys.exc_info(), True
 return wrapped

. This isn't "correct" either, since it doesn't disambiguate between an attempt to close the generator and an explicit throw of an instance of GeneratorExit, which is, though, marked distinctly for usage by the former case. This might be good enough for internal use.

answered May 7, 2020 at 21:38
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.