Skip to content

Commit 2fc8081

Browse files
[3.12] gh-110378: Close invalid generators in contextmanager and asynccontextmanager (GH-110499) (#110588)
contextmanager and asynccontextmanager context managers now close an invalid underlying generator object that yields more then one value. (cherry picked from commit 96fed66) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 3688672 commit 2fc8081

File tree

4 files changed

+43
-7
lines changed

4 files changed

+43
-7
lines changed

Lib/contextlib.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ def __exit__(self, typ, value, traceback):
145145
except StopIteration:
146146
return False
147147
else:
148-
raise RuntimeError("generator didn't stop")
148+
try:
149+
raise RuntimeError("generator didn't stop")
150+
finally:
151+
self.gen.close()
149152
else:
150153
if value is None:
151154
# Need to force instantiation so we can reliably
@@ -187,7 +190,10 @@ def __exit__(self, typ, value, traceback):
187190
raise
188191
exc.__traceback__ = traceback
189192
return False
190-
raise RuntimeError("generator didn't stop after throw()")
193+
try:
194+
raise RuntimeError("generator didn't stop after throw()")
195+
finally:
196+
self.gen.close()
191197

192198
class _AsyncGeneratorContextManager(
193199
_GeneratorContextManagerBase,
@@ -212,7 +218,10 @@ async def __aexit__(self, typ, value, traceback):
212218
except StopAsyncIteration:
213219
return False
214220
else:
215-
raise RuntimeError("generator didn't stop")
221+
try:
222+
raise RuntimeError("generator didn't stop")
223+
finally:
224+
await self.gen.aclose()
216225
else:
217226
if value is None:
218227
# Need to force instantiation so we can reliably
@@ -254,7 +263,10 @@ async def __aexit__(self, typ, value, traceback):
254263
raise
255264
exc.__traceback__ = traceback
256265
return False
257-
raise RuntimeError("generator didn't stop after athrow()")
266+
try:
267+
raise RuntimeError("generator didn't stop after athrow()")
268+
finally:
269+
await self.gen.aclose()
258270

259271

260272
def contextmanager(func):

Lib/test/test_contextlib.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,24 @@ def whoo():
157157
yield
158158
ctx = whoo()
159159
ctx.__enter__()
160-
self.assertRaises(
161-
RuntimeError, ctx.__exit__, TypeError, TypeError("foo"), None
162-
)
160+
with self.assertRaises(RuntimeError):
161+
ctx.__exit__(TypeError, TypeError("foo"), None)
162+
if support.check_impl_detail(cpython=True):
163+
# The "gen" attribute is an implementation detail.
164+
self.assertFalse(ctx.gen.gi_suspended)
165+
166+
def test_contextmanager_trap_second_yield(self):
167+
@contextmanager
168+
def whoo():
169+
yield
170+
yield
171+
ctx = whoo()
172+
ctx.__enter__()
173+
with self.assertRaises(RuntimeError):
174+
ctx.__exit__(None, None, None)
175+
if support.check_impl_detail(cpython=True):
176+
# The "gen" attribute is an implementation detail.
177+
self.assertFalse(ctx.gen.gi_suspended)
163178

164179
def test_contextmanager_except(self):
165180
state = []

Lib/test/test_contextlib_async.py

+6
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ async def whoo():
204204
await ctx.__aenter__()
205205
with self.assertRaises(RuntimeError):
206206
await ctx.__aexit__(TypeError, TypeError('foo'), None)
207+
if support.check_impl_detail(cpython=True):
208+
# The "gen" attribute is an implementation detail.
209+
self.assertFalse(ctx.gen.ag_suspended)
207210

208211
@_async_test
209212
async def test_contextmanager_trap_no_yield(self):
@@ -225,6 +228,9 @@ async def whoo():
225228
await ctx.__aenter__()
226229
with self.assertRaises(RuntimeError):
227230
await ctx.__aexit__(None, None, None)
231+
if support.check_impl_detail(cpython=True):
232+
# The "gen" attribute is an implementation detail.
233+
self.assertFalse(ctx.gen.ag_suspended)
228234

229235
@_async_test
230236
async def test_contextmanager_non_normalised(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`~contextlib.contextmanager` and
2+
:func:`~contextlib.asynccontextmanager` context managers now close an invalid
3+
underlying generator object that yields more then one value.

0 commit comments

Comments
 (0)