Skip to content

Commit 176baa3

Browse files
JelleZijlstra1st1
authored andcommitted
bpo-30241: implement contextlib.AbstractAsyncContextManager (#1412)
1 parent bfbf04e commit 176baa3

File tree

5 files changed

+88
-5
lines changed

5 files changed

+88
-5
lines changed

Doc/library/contextlib.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ Functions and classes provided:
2929
.. versionadded:: 3.6
3030

3131

32+
.. class:: AbstractAsyncContextManager
33+
34+
An :term:`abstract base class` for classes that implement
35+
:meth:`object.__aenter__` and :meth:`object.__aexit__`. A default
36+
implementation for :meth:`object.__aenter__` is provided which returns
37+
``self`` while :meth:`object.__aexit__` is an abstract method which by default
38+
returns ``None``. See also the definition of
39+
:ref:`async-context-managers`.
40+
41+
.. versionadded:: 3.7
42+
3243

3344
.. decorator:: contextmanager
3445

Doc/whatsnew/3.7.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,9 @@ is a list of strings, not bytes.
306306
contextlib
307307
----------
308308

309-
:func:`contextlib.asynccontextmanager` has been added. (Contributed by
310-
Jelle Zijlstra in :issue:`29679`.)
309+
:func:`~contextlib.asynccontextmanager` and
310+
:class:`~contextlib.AbstractAsyncContextManager` have been added. (Contributed
311+
by Jelle Zijlstra in :issue:`29679` and :issue:`30241`.)
311312

312313
cProfile
313314
--------

Lib/contextlib.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from functools import wraps
77

88
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
9-
"AbstractContextManager", "ContextDecorator", "ExitStack",
9+
"AbstractContextManager", "AbstractAsyncContextManager",
10+
"ContextDecorator", "ExitStack",
1011
"redirect_stdout", "redirect_stderr", "suppress"]
1112

1213

@@ -30,6 +31,27 @@ def __subclasshook__(cls, C):
3031
return NotImplemented
3132

3233

34+
class AbstractAsyncContextManager(abc.ABC):
35+
36+
"""An abstract base class for asynchronous context managers."""
37+
38+
async def __aenter__(self):
39+
"""Return `self` upon entering the runtime context."""
40+
return self
41+
42+
@abc.abstractmethod
43+
async def __aexit__(self, exc_type, exc_value, traceback):
44+
"""Raise any exception triggered within the runtime context."""
45+
return None
46+
47+
@classmethod
48+
def __subclasshook__(cls, C):
49+
if cls is AbstractAsyncContextManager:
50+
return _collections_abc._check_methods(C, "__aenter__",
51+
"__aexit__")
52+
return NotImplemented
53+
54+
3355
class ContextDecorator(object):
3456
"A base class or mixin that enables context managers to work as decorators."
3557

@@ -136,7 +158,8 @@ def __exit__(self, type, value, traceback):
136158
raise RuntimeError("generator didn't stop after throw()")
137159

138160

139-
class _AsyncGeneratorContextManager(_GeneratorContextManagerBase):
161+
class _AsyncGeneratorContextManager(_GeneratorContextManagerBase,
162+
AbstractAsyncContextManager):
140163
"""Helper for @asynccontextmanager."""
141164

142165
async def __aenter__(self):

Lib/test/test_contextlib_async.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio
2-
from contextlib import asynccontextmanager
2+
from contextlib import asynccontextmanager, AbstractAsyncContextManager
33
import functools
44
from test import support
55
import unittest
@@ -20,6 +20,53 @@ def wrapper(*args, **kwargs):
2020
return wrapper
2121

2222

23+
class TestAbstractAsyncContextManager(unittest.TestCase):
24+
25+
@_async_test
26+
async def test_enter(self):
27+
class DefaultEnter(AbstractAsyncContextManager):
28+
async def __aexit__(self, *args):
29+
await super().__aexit__(*args)
30+
31+
manager = DefaultEnter()
32+
self.assertIs(await manager.__aenter__(), manager)
33+
34+
async with manager as context:
35+
self.assertIs(manager, context)
36+
37+
def test_exit_is_abstract(self):
38+
class MissingAexit(AbstractAsyncContextManager):
39+
pass
40+
41+
with self.assertRaises(TypeError):
42+
MissingAexit()
43+
44+
def test_structural_subclassing(self):
45+
class ManagerFromScratch:
46+
async def __aenter__(self):
47+
return self
48+
async def __aexit__(self, exc_type, exc_value, traceback):
49+
return None
50+
51+
self.assertTrue(issubclass(ManagerFromScratch, AbstractAsyncContextManager))
52+
53+
class DefaultEnter(AbstractAsyncContextManager):
54+
async def __aexit__(self, *args):
55+
await super().__aexit__(*args)
56+
57+
self.assertTrue(issubclass(DefaultEnter, AbstractAsyncContextManager))
58+
59+
class NoneAenter(ManagerFromScratch):
60+
__aenter__ = None
61+
62+
self.assertFalse(issubclass(NoneAenter, AbstractAsyncContextManager))
63+
64+
class NoneAexit(ManagerFromScratch):
65+
__aexit__ = None
66+
67+
self.assertFalse(issubclass(NoneAexit, AbstractAsyncContextManager))
68+
69+
2370
class AsyncContextManagerTestCase(unittest.TestCase):
2471

2572
@_async_test
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add contextlib.AbstractAsyncContextManager. Patch by Jelle Zijlstra.

0 commit comments

Comments
 (0)