]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.12] gh-110378: Close invalid generators in contextmanager and asynccontextmanager...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 10 Oct 2023 09:12:28 +0000 (11:12 +0200)
committerGitHub <noreply@github.com>
Tue, 10 Oct 2023 09:12:28 +0000 (11:12 +0200)
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 <storchaka@gmail.com>
Lib/contextlib.py
Lib/test/test_contextlib.py
Lib/test/test_contextlib_async.py
Misc/NEWS.d/next/Library/2023-10-07-13-50-12.gh-issue-110378.Y4L8fl.rst [new file with mode: 0644]

index b5acbcb9e6d77cfe5bf1f42fa14905b4d6ae53fb..2efed2d9ec47425605fbb4639cc848566441d465 100644 (file)
@@ -145,7 +145,10 @@ class _GeneratorContextManager(
             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 @@ class _GeneratorContextManager(
                     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 @@ class _AsyncGeneratorContextManager(
             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 @@ class _AsyncGeneratorContextManager(
                     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):
index 0f8351ab8108a64f587d41218a45975efbd8e272..b15829414e5dbba29316afab1b97dc4968f11dd9 100644 (file)
@@ -157,9 +157,24 @@ class ContextManagerTestCase(unittest.TestCase):
                 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 = []
index 3d43ed0fcab1686764695443185509a3686d3738..e519dffee78b44a771b9154affe14dab89187d2b 100644 (file)
@@ -204,6 +204,9 @@ class AsyncContextManagerTestCase(unittest.TestCase):
         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 @@ class AsyncContextManagerTestCase(unittest.TestCase):
         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 (file)
index 0000000..ef5395f
--- /dev/null
@@ -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.