[Python-checkins] bpo-40816 Add AsyncContextDecorator class (GH-20516)

asvetlov webhook-mailer at python.org
Thu Nov 5 03:52:52 EST 2020


https://github.com/python/cpython/commit/178695b7aee7a7aacd49a3086060e06347d1e556
commit: 178695b7aee7a7aacd49a3086060e06347d1e556
branch: master
author: Kazantcev Andrey <45011689+heckad at users.noreply.github.com>
committer: asvetlov <andrew.svetlov at gmail.com>
date: 2020-11-05T10:52:24+02:00
summary:

bpo-40816 Add AsyncContextDecorator class (GH-20516)

Co-authored-by: Yury Selivanov <yury at edgedb.com>

files:
A Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst
M Doc/library/contextlib.rst
M Lib/contextlib.py
M Lib/test/test_contextlib_async.py

diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst
index e42f5a9328166..ee2becb8dff20 100644
--- a/Doc/library/contextlib.rst
+++ b/Doc/library/contextlib.rst
@@ -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)
 
@@ -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
diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index 82ddc1497d863..56b4968118bdb 100644
--- a/Lib/contextlib.py
+++ b/Lib/contextlib.py
@@ -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."""
 
@@ -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__()
diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py
index 3765f6cbf28c5..109807d633d56 100644
--- a/Lib/test/test_contextlib_async.py
+++ b/Lib/test/test_contextlib_async.py
@@ -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):
 
diff --git a/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst b/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst
new file mode 100644
index 0000000000000..66b7577978465
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-05-29-15-25-41.bpo-40816.w61Pob.rst
@@ -0,0 +1 @@
+Add AsyncContextDecorator to contextlib to support async context manager as a decorator.
\ No newline at end of file



More information about the Python-checkins mailing list