From ad7dcbb48c32bdebfcb02c1e940cb7532ee6a6d2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 10 Oct 2023 10:43:04 +0300 Subject: [PATCH 1/2] 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. (cherry picked from commit 96fed66a65097eac2dc528ce29c9ba676bb07689) Co-authored-by: Serhiy Storchaka --- Lib/contextlib.py | 20 ++++++++++++++---- Lib/test/test_contextlib.py | 21 ++++++++++++++++--- Lib/test/test_contextlib_async.py | 6 ++++++ ...-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst | 3 +++ 4 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 58e9a498878d01..4a338f5c637db5 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -145,7 +145,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 @@ -187,7 +190,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, @@ -212,7 +218,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 @@ -254,7 +263,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): diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index ec06785b5667a6..093f2593a4a464 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -156,9 +156,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 = [] diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 3d43ed0fcab168..e519dffee78b44 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -204,6 +204,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_test async def test_contextmanager_trap_no_yield(self): @@ -225,6 +228,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_test async def test_contextmanager_non_normalised(self): diff --git a/Misc/NEWS.d/next/Library/2023-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst b/Misc/NEWS.d/next/Library/2023-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst new file mode 100644 index 00000000000000..ef5395fc3c6420 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst @@ -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. From 1a5db70ddcbe62729ae992fe58a9050d3a89bdbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 10 Oct 2023 10:05:03 +0200 Subject: [PATCH 2/2] Remove asserts using `ag_suspended` --- Lib/test/test_contextlib_async.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index e519dffee78b44..3d43ed0fcab168 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -204,9 +204,6 @@ 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_test async def test_contextmanager_trap_no_yield(self): @@ -228,9 +225,6 @@ 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_test async def test_contextmanager_non_normalised(self):