]> granicus.if.org Git - python/commitdiff
bpo-30241: implement contextlib.AbstractAsyncContextManager (#1412)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Thu, 14 Dec 2017 01:19:17 +0000 (17:19 -0800)
committerYury Selivanov <yury@magic.io>
Thu, 14 Dec 2017 01:19:17 +0000 (20:19 -0500)
Doc/library/contextlib.rst
Doc/whatsnew/3.7.rst
Lib/contextlib.py
Lib/test/test_contextlib_async.py
Misc/NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst [new file with mode: 0644]

index 48ca0da6b95f3ab494207f104eed69f03291631e..faa6c8ac23bd2051ed06aa058d6d02e99bbb6820 100644 (file)
@@ -29,6 +29,17 @@ Functions and classes provided:
    .. versionadded:: 3.6
 
 
+.. class:: AbstractAsyncContextManager
+
+   An :term:`abstract base class` for classes that implement
+   :meth:`object.__aenter__` and :meth:`object.__aexit__`. A default
+   implementation for :meth:`object.__aenter__` is provided which returns
+   ``self`` while :meth:`object.__aexit__` is an abstract method which by default
+   returns ``None``. See also the definition of
+   :ref:`async-context-managers`.
+
+   .. versionadded:: 3.7
+
 
 .. decorator:: contextmanager
 
index 81a88a0c82e54f2ee4428292daf5286a4501ccf5..80f2a7f3a1e291989a301f1003d45a464f42d876 100644 (file)
@@ -306,8 +306,9 @@ is a list of strings, not bytes.
 contextlib
 ----------
 
-:func:`contextlib.asynccontextmanager` has been added. (Contributed by
-Jelle Zijlstra in :issue:`29679`.)
+:func:`~contextlib.asynccontextmanager` and
+:class:`~contextlib.AbstractAsyncContextManager` have been added. (Contributed
+by Jelle Zijlstra in :issue:`29679` and :issue:`30241`.)
 
 cProfile
 --------
index c1f8a84617fce43e5b6af7de4387a81a2cb0b9ea..96c8c22084a339075d2e02f674683b4c6b92e5b0 100644 (file)
@@ -6,7 +6,8 @@ from collections import deque
 from functools import wraps
 
 __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
-           "AbstractContextManager", "ContextDecorator", "ExitStack",
+           "AbstractContextManager", "AbstractAsyncContextManager",
+           "ContextDecorator", "ExitStack",
            "redirect_stdout", "redirect_stderr", "suppress"]
 
 
@@ -30,6 +31,27 @@ class AbstractContextManager(abc.ABC):
         return NotImplemented
 
 
+class AbstractAsyncContextManager(abc.ABC):
+
+    """An abstract base class for asynchronous context managers."""
+
+    async def __aenter__(self):
+        """Return `self` upon entering the runtime context."""
+        return self
+
+    @abc.abstractmethod
+    async def __aexit__(self, exc_type, exc_value, traceback):
+        """Raise any exception triggered within the runtime context."""
+        return None
+
+    @classmethod
+    def __subclasshook__(cls, C):
+        if cls is AbstractAsyncContextManager:
+            return _collections_abc._check_methods(C, "__aenter__",
+                                                   "__aexit__")
+        return NotImplemented
+
+
 class ContextDecorator(object):
     "A base class or mixin that enables context managers to work as decorators."
 
@@ -136,7 +158,8 @@ class _GeneratorContextManager(_GeneratorContextManagerBase,
             raise RuntimeError("generator didn't stop after throw()")
 
 
-class _AsyncGeneratorContextManager(_GeneratorContextManagerBase):
+class _AsyncGeneratorContextManager(_GeneratorContextManagerBase,
+                                    AbstractAsyncContextManager):
     """Helper for @asynccontextmanager."""
 
     async def __aenter__(self):
index 42cc331c0afdb9f9d44e9339d3f89b8705b8e15e..447ca9651222e380646f4d160d6102cd6c67a45e 100644 (file)
@@ -1,5 +1,5 @@
 import asyncio
-from contextlib import asynccontextmanager
+from contextlib import asynccontextmanager, AbstractAsyncContextManager
 import functools
 from test import support
 import unittest
@@ -20,6 +20,53 @@ def _async_test(func):
     return wrapper
 
 
+class TestAbstractAsyncContextManager(unittest.TestCase):
+
+    @_async_test
+    async def test_enter(self):
+        class DefaultEnter(AbstractAsyncContextManager):
+            async def __aexit__(self, *args):
+                await super().__aexit__(*args)
+
+        manager = DefaultEnter()
+        self.assertIs(await manager.__aenter__(), manager)
+
+        async with manager as context:
+            self.assertIs(manager, context)
+
+    def test_exit_is_abstract(self):
+        class MissingAexit(AbstractAsyncContextManager):
+            pass
+
+        with self.assertRaises(TypeError):
+            MissingAexit()
+
+    def test_structural_subclassing(self):
+        class ManagerFromScratch:
+            async def __aenter__(self):
+                return self
+            async def __aexit__(self, exc_type, exc_value, traceback):
+                return None
+
+        self.assertTrue(issubclass(ManagerFromScratch, AbstractAsyncContextManager))
+
+        class DefaultEnter(AbstractAsyncContextManager):
+            async def __aexit__(self, *args):
+                await super().__aexit__(*args)
+
+        self.assertTrue(issubclass(DefaultEnter, AbstractAsyncContextManager))
+
+        class NoneAenter(ManagerFromScratch):
+            __aenter__ = None
+
+        self.assertFalse(issubclass(NoneAenter, AbstractAsyncContextManager))
+
+        class NoneAexit(ManagerFromScratch):
+            __aexit__ = None
+
+        self.assertFalse(issubclass(NoneAexit, AbstractAsyncContextManager))
+
+
 class AsyncContextManagerTestCase(unittest.TestCase):
 
     @_async_test
diff --git a/Misc/NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst b/Misc/NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst
new file mode 100644 (file)
index 0000000..9b6c6f6
--- /dev/null
@@ -0,0 +1 @@
+Add contextlib.AbstractAsyncContextManager. Patch by Jelle Zijlstra.