gh-110378: Close invalid generators in contextmanager and asynccontextmanager (GH-110499)

contextmanager and asynccontextmanager context managers now close an invalid
underlying generator object that yields more then one value.
This commit is contained in:
Serhiy Storchaka 2023-10-10 10:43:04 +03:00 committed by GitHub
parent def7ea5cec
commit 96fed66a65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 7 deletions

View file

@ -149,7 +149,10 @@ def __exit__(self, typ, value, traceback):
except StopIteration:
return False
else:
raise RuntimeError("generator didn't stop")
try:
raise RuntimeError("generator didn't stop")
finally:
self.gen.close()
else:
if value is None:
# Need to force instantiation so we can reliably
@ -191,7 +194,10 @@ def __exit__(self, typ, value, traceback):
raise
exc.__traceback__ = traceback
return False
raise RuntimeError("generator didn't stop after throw()")
try:
raise RuntimeError("generator didn't stop after throw()")
finally:
self.gen.close()
class _AsyncGeneratorContextManager(
_GeneratorContextManagerBase,
@ -216,7 +222,10 @@ async def __aexit__(self, typ, value, traceback):
except StopAsyncIteration:
return False
else:
raise RuntimeError("generator didn't stop")
try:
raise RuntimeError("generator didn't stop")
finally:
await self.gen.aclose()
else:
if value is None:
# Need to force instantiation so we can reliably
@ -258,7 +267,10 @@ async def __aexit__(self, typ, value, traceback):
raise
exc.__traceback__ = traceback
return False
raise RuntimeError("generator didn't stop after athrow()")
try:
raise RuntimeError("generator didn't stop after athrow()")
finally:
await self.gen.aclose()
def contextmanager(func):

View file

@ -167,9 +167,24 @@ def whoo():
yield
ctx = whoo()
ctx.__enter__()
self.assertRaises(
RuntimeError, ctx.__exit__, TypeError, TypeError("foo"), None
)
with self.assertRaises(RuntimeError):
ctx.__exit__(TypeError, TypeError("foo"), None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
self.assertFalse(ctx.gen.gi_suspended)
def test_contextmanager_trap_second_yield(self):
@contextmanager
def whoo():
yield
yield
ctx = whoo()
ctx.__enter__()
with self.assertRaises(RuntimeError):
ctx.__exit__(None, None, None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
self.assertFalse(ctx.gen.gi_suspended)
def test_contextmanager_except(self):
state = []

View file

@ -199,6 +199,9 @@ async def whoo():
await ctx.__aenter__()
with self.assertRaises(RuntimeError):
await ctx.__aexit__(TypeError, TypeError('foo'), None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
self.assertFalse(ctx.gen.ag_suspended)
async def test_contextmanager_trap_no_yield(self):
@asynccontextmanager
@ -218,6 +221,9 @@ async def whoo():
await ctx.__aenter__()
with self.assertRaises(RuntimeError):
await ctx.__aexit__(None, None, None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
self.assertFalse(ctx.gen.ag_suspended)
async def test_contextmanager_non_normalised(self):
@asynccontextmanager

View file

@ -0,0 +1,3 @@
:func:`~contextlib.contextmanager` and
:func:`~contextlib.asynccontextmanager` context managers now close an invalid
underlying generator object that yields more then one value.