Skip to content

Commit

Permalink
bpo-40816 Add AsyncContextDecorator class (pythonGH-20516)
Browse files Browse the repository at this point in the history
Co-authored-by: Yury Selivanov <yury@edgedb.com>
  • Loading branch information
2 people authored and adorilson committed Mar 11, 2021
1 parent 08df9b6 commit f2a1747
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 1 deletion.
62 changes: 62 additions & 0 deletions Doc/library/contextlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ Functions and classes provided:

.. versionadded:: 3.7

Context managers defined with :func:`asynccontextmanager` can be used
either as decorators or with :keyword:`async with` statements::

import time

async def timeit():
now = time.monotonic()
try:
yield
finally:
print(f'it took {time.monotonic() - now}s to run')

@timeit()
async def main():
# ... async code ...

When used as a decorator, a new generator instance is implicitly created on
each function call. This allows the otherwise "one-shot" context managers
created by :func:`asynccontextmanager` to meet the requirement that context
managers support multiple invocations in order to be used as decorators.

.. versionchanged:: 3.10
Async context managers created with :func:`asynccontextmanager` can
be used as decorators.


.. function:: closing(thing)

Expand Down Expand Up @@ -384,6 +409,43 @@ Functions and classes provided:
.. versionadded:: 3.2


.. class:: AsyncContextManager

Similar as ContextManger only for async

Example of ``ContextDecorator``::

from asyncio import run
from contextlib import AsyncContextDecorator

class mycontext(AsyncContextDecorator):
async def __aenter__(self):
print('Starting')
return self

async def __aexit__(self, *exc):
print('Finishing')
return False

>>> @mycontext()
... async def function():
... print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

>>> async def function():
... async with mycontext():
... print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing


.. class:: ExitStack()

A context manager that is designed to make it easy to programmatically
Expand Down
25 changes: 24 additions & 1 deletion Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ def inner(*args, **kwds):
return inner


class AsyncContextDecorator(object):
"A base class or mixin that enables async context managers to work as decorators."

def _recreate_cm(self):
"""Return a recreated instance of self.
"""
return self

def __call__(self, func):
@wraps(func)
async def inner(*args, **kwds):
async with self._recreate_cm():
return await func(*args, **kwds)
return inner


class _GeneratorContextManagerBase:
"""Shared functionality for @contextmanager and @asynccontextmanager."""

Expand Down Expand Up @@ -167,9 +183,16 @@ def __exit__(self, type, value, traceback):


class _AsyncGeneratorContextManager(_GeneratorContextManagerBase,
AbstractAsyncContextManager):
AbstractAsyncContextManager,
AsyncContextDecorator):
"""Helper for @asynccontextmanager."""

def _recreate_cm(self):
# _AGCM instances are one-shot context managers, so the
# ACM must be recreated each time a decorated function is
# called
return self.__class__(self.func, self.args, self.kwds)

async def __aenter__(self):
try:
return await self.gen.__anext__()
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_contextlib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,33 @@ async def woohoo(self, func, args, kwds):
async with woohoo(self=11, func=22, args=33, kwds=44) as target:
self.assertEqual(target, (11, 22, 33, 44))

@_async_test
async def test_recursive(self):
depth = 0
ncols = 0

@asynccontextmanager
async def woohoo():
nonlocal ncols
ncols += 1

nonlocal depth
before = depth
depth += 1
yield
depth -= 1
self.assertEqual(depth, before)

@woohoo()
async def recursive():
if depth < 10:
await recursive()

await recursive()

self.assertEqual(ncols, 10)
self.assertEqual(depth, 0)


class AclosingTestCase(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add AsyncContextDecorator to contextlib to support async context manager as a decorator.

0 comments on commit f2a1747

Please sign in to comment.