]> granicus.if.org Git - python/commitdiff
bpo-33786: Fix asynchronous generators to handle GeneratorExit in athrow() (GH-7467)
authorYury Selivanov <yury@magic.io>
Fri, 8 Jun 2018 00:31:26 +0000 (20:31 -0400)
committerGitHub <noreply@github.com>
Fri, 8 Jun 2018 00:31:26 +0000 (20:31 -0400)
Lib/contextlib.py
Lib/test/test_asyncgen.py
Lib/test/test_contextlib_async.py
Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst [new file with mode: 0644]
Objects/genobject.c

index 1a58b509f67c2818403c2be4951585a037bf0e3d..c06ec73f489d06ddb27b2d1cf7ea174e69a3d744 100644 (file)
@@ -187,7 +187,7 @@ class _AsyncGeneratorContextManager(_GeneratorContextManagerBase,
             # in this implementation
             try:
                 await self.gen.athrow(typ, value, traceback)
-                raise RuntimeError("generator didn't stop after throw()")
+                raise RuntimeError("generator didn't stop after athrow()")
             except StopAsyncIteration as exc:
                 return exc is not value
             except RuntimeError as exc:
index 8dc76ce5c9a173c20688d5d0e138abad8043fb02..9d60cb0e7e8f14664853c90dfd3293f0bbe2fb4c 100644 (file)
@@ -108,6 +108,31 @@ class AsyncGenTest(unittest.TestCase):
                     res.append(str(type(ex)))
             return res
 
+        def async_iterate(g):
+            res = []
+            while True:
+                an = g.__anext__()
+                try:
+                    while True:
+                        try:
+                            an.__next__()
+                        except StopIteration as ex:
+                            if ex.args:
+                                res.append(ex.args[0])
+                                break
+                            else:
+                                res.append('EMPTY StopIteration')
+                                break
+                        except StopAsyncIteration:
+                            raise
+                        except Exception as ex:
+                            res.append(str(type(ex)))
+                            break
+                except StopAsyncIteration:
+                    res.append('STOP')
+                    break
+            return res
+
         def async_iterate(g):
             res = []
             while True:
@@ -297,6 +322,37 @@ class AsyncGenTest(unittest.TestCase):
                                     "non-None value .* async generator"):
             gen().__anext__().send(100)
 
+    def test_async_gen_exception_11(self):
+        def sync_gen():
+            yield 10
+            yield 20
+
+        def sync_gen_wrapper():
+            yield 1
+            sg = sync_gen()
+            sg.send(None)
+            try:
+                sg.throw(GeneratorExit())
+            except GeneratorExit:
+                yield 2
+            yield 3
+
+        async def async_gen():
+            yield 10
+            yield 20
+
+        async def async_gen_wrapper():
+            yield 1
+            asg = async_gen()
+            await asg.asend(None)
+            try:
+                await asg.athrow(GeneratorExit())
+            except GeneratorExit:
+                yield 2
+            yield 3
+
+        self.compare_generators(sync_gen_wrapper(), async_gen_wrapper())
+
     def test_async_gen_api_01(self):
         async def gen():
             yield 123
index 355955f9ab85bed5f6f893de7c5ba68328fee697..39dcd9b364e589106d5243eb54c14e222ab87f92 100644 (file)
@@ -36,6 +36,28 @@ class TestAbstractAsyncContextManager(unittest.TestCase):
         async with manager as context:
             self.assertIs(manager, context)
 
+    @_async_test
+    async def test_async_gen_propagates_generator_exit(self):
+        # A regression test for https://bugs.python.org/issue33786.
+
+        @asynccontextmanager
+        async def ctx():
+            yield
+
+        async def gen():
+            async with ctx():
+                yield 11
+
+        ret = []
+        exc = ValueError(22)
+        with self.assertRaises(ValueError):
+            async with ctx():
+                async for val in gen():
+                    ret.append(val)
+                    raise exc
+
+        self.assertEqual(ret, [11])
+
     def test_exit_is_abstract(self):
         class MissingAexit(AbstractAsyncContextManager):
             pass
diff --git a/Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst b/Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst
new file mode 100644 (file)
index 0000000..57deefe
--- /dev/null
@@ -0,0 +1 @@
+Fix asynchronous generators to handle GeneratorExit in athrow() correctly
index 9f593382f56a59cf73867577970ed78adf837a55..e55cfd21c69c2cf8868830125d67c7a325f47a4c 100644 (file)
@@ -1876,21 +1876,20 @@ yield_close:
     return NULL;
 
 check_error:
-    if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
+    if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
+            PyErr_ExceptionMatches(PyExc_GeneratorExit))
+    {
         o->agt_state = AWAITABLE_STATE_CLOSED;
         if (o->agt_args == NULL) {
             /* when aclose() is called we don't want to propagate
-               StopAsyncIteration; just raise StopIteration, signalling
-               that 'aclose()' is done. */
+               StopAsyncIteration or GeneratorExit; just raise
+               StopIteration, signalling that this 'aclose()' await
+               is done.
+            */
             PyErr_Clear();
             PyErr_SetNone(PyExc_StopIteration);
         }
     }
-    else if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
-        o->agt_state = AWAITABLE_STATE_CLOSED;
-        PyErr_Clear();          /* ignore these errors */
-        PyErr_SetNone(PyExc_StopIteration);
-    }
     return NULL;
 }