]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-110378: Close invalid generators in contextmanager and asynccontextmanager (GH...
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 10 Oct 2023 07:43:04 +0000 (10:43 +0300)
committerGitHub <noreply@github.com>
Tue, 10 Oct 2023 07:43:04 +0000 (09:43 +0200)
contextmanager and asynccontextmanager context managers now close an invalid
underlying generator object that yields more then one value.

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 f82e7bca35735b2ddf76b1823f15adaa45570ab4..6994690ebf7eb2a8c87ac6092cbe7cc0c1330e70 100644 (file)
@@ -149,7 +149,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
@@ -191,7 +194,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,
@@ -216,7 +222,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
@@ -258,7 +267,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 dbc7dfcc24bf0767e02ade1909fc220da86d3442..5d94ec7cae47069c7ac55670f90784a1ddcb967f 100644 (file)
@@ -167,9 +167,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 8ccce5ae1e7e7c8761e18a149fbda33e4edd7789..540964a9bfdcfde167165f0a2c146e1344adf569 100644 (file)
@@ -199,6 +199,9 @@ class AsyncContextManagerTestCase(unittest.IsolatedAsyncioTestCase):
         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 @@ class AsyncContextManagerTestCase(unittest.IsolatedAsyncioTestCase):
         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
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.