From b0f92e538992e4f38b93fff9defe1bb78c23ece3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 1 May 2017 08:59:47 -0700 Subject: [PATCH 1/4] implement contextlib.AbstractAsyncContextManager --- Doc/library/contextlib.rst | 9 ++++++++ Doc/whatsnew/3.7.rst | 5 +++-- Lib/contextlib.py | 28 ++++++++++++++++++++++-- Lib/test/test_contextlib_async.py | 36 ++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index 19793693b7ba68..be0a480c5d1deb 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -29,6 +29,15 @@ Functions and classes provided: .. versionadded:: 3.6 +.. class:: AbstractAsyncContextManager + + An :term:`abstract base class` similar to + :class:`~contextlib.AbstractContextManager`, but for + :ref:`asynchronous context managers `, which + implement :meth:`object.__aenter__` and :meth:`object.__aexit__`. + + .. versionadded:: 3.7 + .. decorator:: contextmanager diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 7edf4fc3cf4269..b1802477dab970 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -105,8 +105,9 @@ instead of spaces. (Contributed by Xiang Zhang in :issue:`30103`.) 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`.) distutils --------- diff --git a/Lib/contextlib.py b/Lib/contextlib.py index c53b35e8d5adaa..f3cd4b2e518b03 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -5,7 +5,8 @@ from functools import wraps __all__ = ["asynccontextmanager", "contextmanager", "closing", - "AbstractContextManager", "ContextDecorator", "ExitStack", + "AbstractContextManager", "AbstractAsyncContextManager", + "ContextDecorator", "ExitStack", "redirect_stdout", "redirect_stderr", "suppress"] @@ -31,6 +32,28 @@ def __subclasshook__(cls, C): 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: + if (any("__aenter__" in B.__dict__ for B in C.__mro__) and + any("__aexit__" in B.__dict__ for B in C.__mro__)): + return True + return NotImplemented + + class ContextDecorator(object): "A base class or mixin that enables context managers to work as decorators." @@ -137,7 +160,8 @@ def __exit__(self, type, value, traceback): raise RuntimeError("generator didn't stop after throw()") -class _AsyncGeneratorContextManager(_GeneratorContextManagerBase): +class _AsyncGeneratorContextManager(_GeneratorContextManagerBase, + AbstractAsyncContextManager): """Helper for @asynccontextmanager.""" async def __aenter__(self): diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 42cc331c0afdb9..2898ce98321c9e 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -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,40 @@ def wrapper(*args, **kwargs): 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) + + 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 AsyncContextManagerTestCase(unittest.TestCase): @_async_test From 9fb10286b0707924ba69633812f79b0c04c5c6c1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 3 May 2017 20:33:38 -0700 Subject: [PATCH 2/4] support the "= None" pattern --- Lib/contextlib.py | 6 +++--- Lib/test/test_contextlib_async.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/contextlib.py b/Lib/contextlib.py index f3cd4b2e518b03..30a409cf8de6f0 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -1,6 +1,7 @@ """Utilities for with-statement contexts. See PEP 343.""" import abc import sys +import _collections_abc from collections import deque from functools import wraps @@ -48,9 +49,8 @@ async def __aexit__(self, exc_type, exc_value, traceback): @classmethod def __subclasshook__(cls, C): if cls is AbstractAsyncContextManager: - if (any("__aenter__" in B.__dict__ for B in C.__mro__) and - any("__aexit__" in B.__dict__ for B in C.__mro__)): - return True + return _collections_abc._check_methods(C, "__aenter__", + "__aexit__") return NotImplemented diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 2898ce98321c9e..447ca9651222e3 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -31,6 +31,9 @@ async def __aexit__(self, *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 @@ -53,6 +56,16 @@ async def __aexit__(self, *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): From 290802ffb8cacea0f654ab31e84a43198a3f452f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 10 Oct 2017 18:56:51 -0700 Subject: [PATCH 3/4] add NEWS entry --- .../NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst 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 index 00000000000000..9b6c6f6e422cc1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-10-18-56-46.bpo-30241.F_go20.rst @@ -0,0 +1 @@ +Add contextlib.AbstractAsyncContextManager. Patch by Jelle Zijlstra. From 171c597add243223bfb5fdeda8521c82469c8317 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 13 Dec 2017 16:47:40 -0800 Subject: [PATCH 4/4] rewrite docs as suggested by @1st1 --- Doc/library/contextlib.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/library/contextlib.rst b/Doc/library/contextlib.rst index be0a480c5d1deb..cc987fb357e2ae 100644 --- a/Doc/library/contextlib.rst +++ b/Doc/library/contextlib.rst @@ -31,10 +31,12 @@ Functions and classes provided: .. class:: AbstractAsyncContextManager - An :term:`abstract base class` similar to - :class:`~contextlib.AbstractContextManager`, but for - :ref:`asynchronous context managers `, which - implement :meth:`object.__aenter__` and :meth:`object.__aexit__`. + 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