[Python-checkins] bpo-33786: Fix asynchronous generators to handle GeneratorExit in athrow() (GH-7467) (GH-21878)

Miss Islington (bot) webhook-mailer at python.org
Fri Aug 14 05:44:13 EDT 2020


https://github.com/python/cpython/commit/cf79cbf4479e395bf7c4df2907f5a444639b4f6f
commit: cf79cbf4479e395bf7c4df2907f5a444639b4f6f
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2020-08-14T05:44:00-04:00
summary:

bpo-33786: Fix asynchronous generators to handle GeneratorExit in athrow() (GH-7467) (GH-21878)

(cherry picked from commit 52698c7ad9eae9feb35839fde17a7d1da8036a9b)

Co-authored-by: Yury Selivanov <yury at magic.io>

files:
A Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst
M Lib/contextlib.py
M Lib/test/test_asyncgen.py
M Lib/test/test_contextlib_async.py
M Objects/genobject.c

diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index 2d745ea3e3c55..13a3e1a5536c6 100644
--- a/Lib/contextlib.py
+++ b/Lib/contextlib.py
@@ -186,7 +186,7 @@ async def __aexit__(self, typ, value, traceback):
             # 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:
diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py
index 5a292fb5d64e1..5da27417f2d7d 100644
--- a/Lib/test/test_asyncgen.py
+++ b/Lib/test/test_asyncgen.py
@@ -108,6 +108,31 @@ def sync_iterate(g):
                     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 @@ async def gen():
                                     "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
diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py
index cc38dcf8c45c8..9db40652f40f5 100644
--- a/Lib/test/test_contextlib_async.py
+++ b/Lib/test/test_contextlib_async.py
@@ -36,6 +36,28 @@ async def __aexit__(self, *args):
         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
index 0000000000000..57deefe339b5c
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst	
@@ -0,0 +1 @@
+Fix asynchronous generators to handle GeneratorExit in athrow() correctly
diff --git a/Objects/genobject.c b/Objects/genobject.c
index dd7d44bd427df..b11690cde3e11 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -1893,21 +1893,20 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
     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;
 }
 



More information about the Python-checkins mailing list