Description
Suppose we define a context manager, in the usual way:
@contextmanager def ctx(): print("enter") yield print("exit") def classic(): with ctx(): print("body") classic() # enter, body, exit
We can also use it as a decorator, thanks to the convenient ContextDecorator
class used by @contextmanager
:
@ctx() def fn(): print("body") fn() # enter, body, exit
...but if we naively do the same thing to a generator or an async function, the equivalence breaks down:
@ctx() def gen(): print("body") yield for _ in gen(): ... # enter, exit, body! @ctx() async def afn(): print("body") await afn() # enter, exit, body!
This seems pretty obviously undesirable, so I think we'll want to change ContextDecorator.__call__
. Possibilities include:
-
branch on iscoroutinefunction / isgeneratorfunction / isasyncgenfunction, creating alternative
inner
functions to preserve the invariant that@ctx()
is just like writingwith ctx():
as the first line of the function body. In a quick survey, this is the expected behavior, but also a change from the current effect of the decorator. -
instead of branching, warn (and after a few years raise) when decorating a generator or async function.
- alternative implementation: inspect the return value inside
inner
, to detect sync wrappers of async functions. I think the increased accuracy is unlikely to be worth the performance cost. - we could retain the convenience of using a decorator by defining
ContextDecorator.gen()
,.async_()
, and.agen()
methods which explicitly support wrapping their corresponding kind of function.
- alternative implementation: inspect the return value inside
We'll also want applying an AsyncContextDecorator
to an async generator function to match whatever we decide for ContextDecorator
on a sync generator.