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.