Skip to content

Commit 2e62469

Browse files
JelleZijlstra1st1
authored andcommitted
bpo-29679: Implement @contextlib.asynccontextmanager (#360)
1 parent 9dc2b38 commit 2e62469

File tree

5 files changed

+343
-6
lines changed

5 files changed

+343
-6
lines changed

Doc/library/contextlib.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,36 @@ Functions and classes provided:
8080
Use of :class:`ContextDecorator`.
8181

8282

83+
.. decorator:: asynccontextmanager
84+
85+
Similar to :func:`~contextlib.contextmanager`, but creates an
86+
:ref:`asynchronous context manager <async-context-managers>`.
87+
88+
This function is a :term:`decorator` that can be used to define a factory
89+
function for :keyword:`async with` statement asynchronous context managers,
90+
without needing to create a class or separate :meth:`__aenter__` and
91+
:meth:`__aexit__` methods. It must be applied to an :term:`asynchronous
92+
generator` function.
93+
94+
A simple example::
95+
96+
from contextlib import asynccontextmanager
97+
98+
@asynccontextmanager
99+
async def get_connection():
100+
conn = await acquire_db_connection()
101+
try:
102+
yield
103+
finally:
104+
await release_db_connection(conn)
105+
106+
async def get_all_users():
107+
async with get_connection() as conn:
108+
return conn.query('SELECT ...')
109+
110+
.. versionadded:: 3.7
111+
112+
83113
.. function:: closing(thing)
84114

85115
Return a context manager that closes *thing* upon completion of the block. This

Doc/reference/datamodel.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,6 +2575,8 @@ An example of an asynchronous iterable object::
25752575
result in a :exc:`RuntimeError`.
25762576

25772577

2578+
.. _async-context-managers:
2579+
25782580
Asynchronous Context Managers
25792581
-----------------------------
25802582

Doc/whatsnew/3.7.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ New Modules
9595
Improved Modules
9696
================
9797

98+
contextlib
99+
----------
100+
101+
:func:`contextlib.asynccontextmanager` has been added. (Contributed by
102+
Jelle Zijlstra in :issue:`29679`.)
103+
98104
distutils
99105
---------
100106

Lib/contextlib.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from collections import deque
55
from functools import wraps
66

7-
__all__ = ["contextmanager", "closing", "AbstractContextManager",
8-
"ContextDecorator", "ExitStack", "redirect_stdout",
9-
"redirect_stderr", "suppress"]
7+
__all__ = ["asynccontextmanager", "contextmanager", "closing",
8+
"AbstractContextManager", "ContextDecorator", "ExitStack",
9+
"redirect_stdout", "redirect_stderr", "suppress"]
1010

1111

1212
class AbstractContextManager(abc.ABC):
@@ -54,8 +54,8 @@ def inner(*args, **kwds):
5454
return inner
5555

5656

57-
class _GeneratorContextManager(ContextDecorator, AbstractContextManager):
58-
"""Helper for @contextmanager decorator."""
57+
class _GeneratorContextManagerBase:
58+
"""Shared functionality for @contextmanager and @asynccontextmanager."""
5959

6060
def __init__(self, func, args, kwds):
6161
self.gen = func(*args, **kwds)
@@ -71,6 +71,12 @@ def __init__(self, func, args, kwds):
7171
# for the class instead.
7272
# See http://bugs.python.org/issue19404 for more details.
7373

74+
75+
class _GeneratorContextManager(_GeneratorContextManagerBase,
76+
AbstractContextManager,
77+
ContextDecorator):
78+
"""Helper for @contextmanager decorator."""
79+
7480
def _recreate_cm(self):
7581
# _GCM instances are one-shot context managers, so the
7682
# CM must be recreated each time a decorated function is
@@ -121,12 +127,61 @@ def __exit__(self, type, value, traceback):
121127
# fixes the impedance mismatch between the throw() protocol
122128
# and the __exit__() protocol.
123129
#
130+
# This cannot use 'except BaseException as exc' (as in the
131+
# async implementation) to maintain compatibility with
132+
# Python 2, where old-style class exceptions are not caught
133+
# by 'except BaseException'.
124134
if sys.exc_info()[1] is value:
125135
return False
126136
raise
127137
raise RuntimeError("generator didn't stop after throw()")
128138

129139

140+
class _AsyncGeneratorContextManager(_GeneratorContextManagerBase):
141+
"""Helper for @asynccontextmanager."""
142+
143+
async def __aenter__(self):
144+
try:
145+
return await self.gen.__anext__()
146+
except StopAsyncIteration:
147+
raise RuntimeError("generator didn't yield") from None
148+
149+
async def __aexit__(self, typ, value, traceback):
150+
if typ is None:
151+
try:
152+
await self.gen.__anext__()
153+
except StopAsyncIteration:
154+
return
155+
else:
156+
raise RuntimeError("generator didn't stop")
157+
else:
158+
if value is None:
159+
value = typ()
160+
# See _GeneratorContextManager.__exit__ for comments on subtleties
161+
# in this implementation
162+
try:
163+
await self.gen.athrow(typ, value, traceback)
164+
raise RuntimeError("generator didn't stop after throw()")
165+
except StopAsyncIteration as exc:
166+
return exc is not value
167+
except RuntimeError as exc:
168+
if exc is value:
169+
return False
170+
# Avoid suppressing if a StopIteration exception
171+
# was passed to throw() and later wrapped into a RuntimeError
172+
# (see PEP 479 for sync generators; async generators also
173+
# have this behavior). But do this only if the exception wrapped
174+
# by the RuntimeError is actully Stop(Async)Iteration (see
175+
# issue29692).
176+
if isinstance(value, (StopIteration, StopAsyncIteration)):
177+
if exc.__cause__ is value:
178+
return False
179+
raise
180+
except BaseException as exc:
181+
if exc is not value:
182+
raise
183+
184+
130185
def contextmanager(func):
131186
"""@contextmanager decorator.
132187
@@ -153,14 +208,46 @@ def some_generator(<arguments>):
153208
<body>
154209
finally:
155210
<cleanup>
156-
157211
"""
158212
@wraps(func)
159213
def helper(*args, **kwds):
160214
return _GeneratorContextManager(func, args, kwds)
161215
return helper
162216

163217

218+
def asynccontextmanager(func):
219+
"""@asynccontextmanager decorator.
220+
221+
Typical usage:
222+
223+
@asynccontextmanager
224+
async def some_async_generator(<arguments>):
225+
<setup>
226+
try:
227+
yield <value>
228+
finally:
229+
<cleanup>
230+
231+
This makes this:
232+
233+
async with some_async_generator(<arguments>) as <variable>:
234+
<body>
235+
236+
equivalent to this:
237+
238+
<setup>
239+
try:
240+
<variable> = <value>
241+
<body>
242+
finally:
243+
<cleanup>
244+
"""
245+
@wraps(func)
246+
def helper(*args, **kwds):
247+
return _AsyncGeneratorContextManager(func, args, kwds)
248+
return helper
249+
250+
164251
class closing(AbstractContextManager):
165252
"""Context to automatically close something at the end of a block.
166253

0 commit comments

Comments
 (0)