Skip to content

Commit 68b4690

Browse files
bpo-44566: resolve differences between asynccontextmanager and contextmanager (GH-27024) (#27266)
(cherry picked from commit 7f1c330) Co-authored-by: Thomas Grainger <tagrain@gmail.com>
1 parent 574da46 commit 68b4690

File tree

4 files changed

+85
-56
lines changed

4 files changed

+85
-56
lines changed

Lib/contextlib.py

+59-45
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,20 @@ def __init__(self, func, args, kwds):
113113
# for the class instead.
114114
# See http://bugs.python.org/issue19404 for more details.
115115

116-
117-
class _GeneratorContextManager(_GeneratorContextManagerBase,
118-
AbstractContextManager,
119-
ContextDecorator):
120-
"""Helper for @contextmanager decorator."""
121-
122116
def _recreate_cm(self):
123-
# _GCM instances are one-shot context managers, so the
117+
# _GCMB instances are one-shot context managers, so the
124118
# CM must be recreated each time a decorated function is
125119
# called
126120
return self.__class__(self.func, self.args, self.kwds)
127121

122+
123+
class _GeneratorContextManager(
124+
_GeneratorContextManagerBase,
125+
AbstractContextManager,
126+
ContextDecorator,
127+
):
128+
"""Helper for @contextmanager decorator."""
129+
128130
def __enter__(self):
129131
# do not keep args and kwds alive unnecessarily
130132
# they are only needed for recreation, which is not possible anymore
@@ -134,8 +136,8 @@ def __enter__(self):
134136
except StopIteration:
135137
raise RuntimeError("generator didn't yield") from None
136138

137-
def __exit__(self, type, value, traceback):
138-
if type is None:
139+
def __exit__(self, typ, value, traceback):
140+
if typ is None:
139141
try:
140142
next(self.gen)
141143
except StopIteration:
@@ -146,9 +148,9 @@ def __exit__(self, type, value, traceback):
146148
if value is None:
147149
# Need to force instantiation so we can reliably
148150
# tell if we get the same exception back
149-
value = type()
151+
value = typ()
150152
try:
151-
self.gen.throw(type, value, traceback)
153+
self.gen.throw(typ, value, traceback)
152154
except StopIteration as exc:
153155
# Suppress StopIteration *unless* it's the same exception that
154156
# was passed to throw(). This prevents a StopIteration
@@ -158,81 +160,93 @@ def __exit__(self, type, value, traceback):
158160
# Don't re-raise the passed in exception. (issue27122)
159161
if exc is value:
160162
return False
161-
# Likewise, avoid suppressing if a StopIteration exception
163+
# Avoid suppressing if a StopIteration exception
162164
# was passed to throw() and later wrapped into a RuntimeError
163-
# (see PEP 479).
164-
if type is StopIteration and exc.__cause__ is value:
165+
# (see PEP 479 for sync generators; async generators also
166+
# have this behavior). But do this only if the exception wrapped
167+
# by the RuntimeError is actually Stop(Async)Iteration (see
168+
# issue29692).
169+
if (
170+
isinstance(value, StopIteration)
171+
and exc.__cause__ is value
172+
):
165173
return False
166174
raise
167-
except:
175+
except BaseException as exc:
168176
# only re-raise if it's *not* the exception that was
169177
# passed to throw(), because __exit__() must not raise
170178
# an exception unless __exit__() itself failed. But throw()
171179
# has to raise the exception to signal propagation, so this
172180
# fixes the impedance mismatch between the throw() protocol
173181
# and the __exit__() protocol.
174-
#
175-
# This cannot use 'except BaseException as exc' (as in the
176-
# async implementation) to maintain compatibility with
177-
# Python 2, where old-style class exceptions are not caught
178-
# by 'except BaseException'.
179-
if sys.exc_info()[1] is value:
180-
return False
181-
raise
182+
if exc is not value:
183+
raise
184+
return False
182185
raise RuntimeError("generator didn't stop after throw()")
183186

184-
185-
class _AsyncGeneratorContextManager(_GeneratorContextManagerBase,
186-
AbstractAsyncContextManager,
187-
AsyncContextDecorator):
188-
"""Helper for @asynccontextmanager."""
189-
190-
def _recreate_cm(self):
191-
# _AGCM instances are one-shot context managers, so the
192-
# ACM must be recreated each time a decorated function is
193-
# called
194-
return self.__class__(self.func, self.args, self.kwds)
187+
class _AsyncGeneratorContextManager(
188+
_GeneratorContextManagerBase,
189+
AbstractAsyncContextManager,
190+
AsyncContextDecorator,
191+
):
192+
"""Helper for @asynccontextmanager decorator."""
195193

196194
async def __aenter__(self):
195+
# do not keep args and kwds alive unnecessarily
196+
# they are only needed for recreation, which is not possible anymore
197+
del self.args, self.kwds, self.func
197198
try:
198-
return await self.gen.__anext__()
199+
return await anext(self.gen)
199200
except StopAsyncIteration:
200201
raise RuntimeError("generator didn't yield") from None
201202

202203
async def __aexit__(self, typ, value, traceback):
203204
if typ is None:
204205
try:
205-
await self.gen.__anext__()
206+
await anext(self.gen)
206207
except StopAsyncIteration:
207-
return
208+
return False
208209
else:
209210
raise RuntimeError("generator didn't stop")
210211
else:
211212
if value is None:
213+
# Need to force instantiation so we can reliably
214+
# tell if we get the same exception back
212215
value = typ()
213-
# See _GeneratorContextManager.__exit__ for comments on subtleties
214-
# in this implementation
215216
try:
216217
await self.gen.athrow(typ, value, traceback)
217-
raise RuntimeError("generator didn't stop after athrow()")
218218
except StopAsyncIteration as exc:
219+
# Suppress StopIteration *unless* it's the same exception that
220+
# was passed to throw(). This prevents a StopIteration
221+
# raised inside the "with" statement from being suppressed.
219222
return exc is not value
220223
except RuntimeError as exc:
224+
# Don't re-raise the passed in exception. (issue27122)
221225
if exc is value:
222226
return False
223-
# Avoid suppressing if a StopIteration exception
224-
# was passed to throw() and later wrapped into a RuntimeError
227+
# Avoid suppressing if a Stop(Async)Iteration exception
228+
# was passed to athrow() and later wrapped into a RuntimeError
225229
# (see PEP 479 for sync generators; async generators also
226230
# have this behavior). But do this only if the exception wrapped
227231
# by the RuntimeError is actully Stop(Async)Iteration (see
228232
# issue29692).
229-
if isinstance(value, (StopIteration, StopAsyncIteration)):
230-
if exc.__cause__ is value:
231-
return False
233+
if (
234+
isinstance(value, (StopIteration, StopAsyncIteration))
235+
and exc.__cause__ is value
236+
):
237+
return False
232238
raise
233239
except BaseException as exc:
240+
# only re-raise if it's *not* the exception that was
241+
# passed to throw(), because __exit__() must not raise
242+
# an exception unless __exit__() itself failed. But throw()
243+
# has to raise the exception to signal propagation, so this
244+
# fixes the impedance mismatch between the throw() protocol
245+
# and the __exit__() protocol.
234246
if exc is not value:
235247
raise
248+
return False
249+
raise RuntimeError("generator didn't stop after athrow()")
236250

237251

238252
def contextmanager(func):

Lib/test/test_contextlib.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,22 @@ def woohoo():
126126
self.assertEqual(state, [1, 42, 999])
127127

128128
def test_contextmanager_except_stopiter(self):
129-
stop_exc = StopIteration('spam')
130129
@contextmanager
131130
def woohoo():
132131
yield
133-
try:
134-
with self.assertWarnsRegex(DeprecationWarning,
135-
"StopIteration"):
136-
with woohoo():
137-
raise stop_exc
138-
except Exception as ex:
139-
self.assertIs(ex, stop_exc)
140-
else:
141-
self.fail('StopIteration was suppressed')
132+
133+
class StopIterationSubclass(StopIteration):
134+
pass
135+
136+
for stop_exc in (StopIteration('spam'), StopIterationSubclass('spam')):
137+
with self.subTest(type=type(stop_exc)):
138+
try:
139+
with woohoo():
140+
raise stop_exc
141+
except Exception as ex:
142+
self.assertIs(ex, stop_exc)
143+
else:
144+
self.fail(f'{stop_exc} was suppressed')
142145

143146
def test_contextmanager_except_pep479(self):
144147
code = """\

Lib/test/test_contextlib_async.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,18 @@ async def test_contextmanager_except_stopiter(self):
209209
async def woohoo():
210210
yield
211211

212-
for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):
212+
class StopIterationSubclass(StopIteration):
213+
pass
214+
215+
class StopAsyncIterationSubclass(StopAsyncIteration):
216+
pass
217+
218+
for stop_exc in (
219+
StopIteration('spam'),
220+
StopAsyncIteration('ham'),
221+
StopIterationSubclass('spam'),
222+
StopAsyncIterationSubclass('spam')
223+
):
213224
with self.subTest(type=type(stop_exc)):
214225
try:
215226
async with woohoo():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
handle StopIteration subclass raised from @contextlib.contextmanager generator

0 commit comments

Comments
 (0)