From 4a1e66089418324d586aed7447a8cf4d757b0ce9 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Wed, 6 Jun 2018 23:24:55 -0400 Subject: [PATCH 1/3] bpo-33786: Fix asynchronous generators to handle GeneratorExit in athrow() --- Lib/contextlib.py | 2 +- Lib/test/test_asyncgen.py | 31 +++++++++++++++++++ .../2018-06-06-23-24-40.bpo-33786.lBvT8z.rst | 1 + Objects/genobject.c | 15 +++++---- 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2018-06-06-23-24-40.bpo-33786.lBvT8z.rst diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 1a58b509f67c28..c06ec73f489d06 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -187,7 +187,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 8dc76ce5c9a173..1df20c10c4320a 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -297,6 +297,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/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 00000000000000..57deefe339b5c8 --- /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 9f593382f56a59..e55cfd21c69c2c 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -1876,21 +1876,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; } From e1aa01908d7f291fc067d4d34822fbc76b09e5ac Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 7 Jun 2018 11:00:22 -0400 Subject: [PATCH 2/3] Add a contextlib-specific regression test --- Lib/test/test_contextlib_async.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 355955f9ab85be..39dcd9b364e589 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 From 81827b3facd54a44be2f6e932ef1bcc2c5b3e339 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 7 Jun 2018 11:03:34 -0400 Subject: [PATCH 3/3] Make compare_generators test helper use async iter protocol correctly --- Lib/test/test_asyncgen.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index 1df20c10c4320a..9d60cb0e7e8f14 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: