Skip to content

Commit cf79cbf

Browse files
miss-islington1st1
andauthored
bpo-33786: Fix asynchronous generators to handle GeneratorExit in athrow() (GH-7467) (GH-21878)
(cherry picked from commit 52698c7) Co-authored-by: Yury Selivanov <yury@magic.io>
1 parent f3b6f3c commit cf79cbf

File tree

5 files changed

+87
-9
lines changed

5 files changed

+87
-9
lines changed

Lib/contextlib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ async def __aexit__(self, typ, value, traceback):
186186
# in this implementation
187187
try:
188188
await self.gen.athrow(typ, value, traceback)
189-
raise RuntimeError("generator didn't stop after throw()")
189+
raise RuntimeError("generator didn't stop after athrow()")
190190
except StopAsyncIteration as exc:
191191
return exc is not value
192192
except RuntimeError as exc:

Lib/test/test_asyncgen.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ def sync_iterate(g):
108108
res.append(str(type(ex)))
109109
return res
110110

111+
def async_iterate(g):
112+
res = []
113+
while True:
114+
an = g.__anext__()
115+
try:
116+
while True:
117+
try:
118+
an.__next__()
119+
except StopIteration as ex:
120+
if ex.args:
121+
res.append(ex.args[0])
122+
break
123+
else:
124+
res.append('EMPTY StopIteration')
125+
break
126+
except StopAsyncIteration:
127+
raise
128+
except Exception as ex:
129+
res.append(str(type(ex)))
130+
break
131+
except StopAsyncIteration:
132+
res.append('STOP')
133+
break
134+
return res
135+
111136
def async_iterate(g):
112137
res = []
113138
while True:
@@ -297,6 +322,37 @@ async def gen():
297322
"non-None value .* async generator"):
298323
gen().__anext__().send(100)
299324

325+
def test_async_gen_exception_11(self):
326+
def sync_gen():
327+
yield 10
328+
yield 20
329+
330+
def sync_gen_wrapper():
331+
yield 1
332+
sg = sync_gen()
333+
sg.send(None)
334+
try:
335+
sg.throw(GeneratorExit())
336+
except GeneratorExit:
337+
yield 2
338+
yield 3
339+
340+
async def async_gen():
341+
yield 10
342+
yield 20
343+
344+
async def async_gen_wrapper():
345+
yield 1
346+
asg = async_gen()
347+
await asg.asend(None)
348+
try:
349+
await asg.athrow(GeneratorExit())
350+
except GeneratorExit:
351+
yield 2
352+
yield 3
353+
354+
self.compare_generators(sync_gen_wrapper(), async_gen_wrapper())
355+
300356
def test_async_gen_api_01(self):
301357
async def gen():
302358
yield 123

Lib/test/test_contextlib_async.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ async def __aexit__(self, *args):
3636
async with manager as context:
3737
self.assertIs(manager, context)
3838

39+
@_async_test
40+
async def test_async_gen_propagates_generator_exit(self):
41+
# A regression test for https://bugs.python.org/issue33786.
42+
43+
@asynccontextmanager
44+
async def ctx():
45+
yield
46+
47+
async def gen():
48+
async with ctx():
49+
yield 11
50+
51+
ret = []
52+
exc = ValueError(22)
53+
with self.assertRaises(ValueError):
54+
async with ctx():
55+
async for val in gen():
56+
ret.append(val)
57+
raise exc
58+
59+
self.assertEqual(ret, [11])
60+
3961
def test_exit_is_abstract(self):
4062
class MissingAexit(AbstractAsyncContextManager):
4163
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix asynchronous generators to handle GeneratorExit in athrow() correctly

Objects/genobject.c

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,21 +1893,20 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
18931893
return NULL;
18941894

18951895
check_error:
1896-
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
1896+
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
1897+
PyErr_ExceptionMatches(PyExc_GeneratorExit))
1898+
{
18971899
o->agt_state = AWAITABLE_STATE_CLOSED;
18981900
if (o->agt_args == NULL) {
18991901
/* when aclose() is called we don't want to propagate
1900-
StopAsyncIteration; just raise StopIteration, signalling
1901-
that 'aclose()' is done. */
1902+
StopAsyncIteration or GeneratorExit; just raise
1903+
StopIteration, signalling that this 'aclose()' await
1904+
is done.
1905+
*/
19021906
PyErr_Clear();
19031907
PyErr_SetNone(PyExc_StopIteration);
19041908
}
19051909
}
1906-
else if (PyErr_ExceptionMatches(PyExc_GeneratorExit)) {
1907-
o->agt_state = AWAITABLE_STATE_CLOSED;
1908-
PyErr_Clear(); /* ignore these errors */
1909-
PyErr_SetNone(PyExc_StopIteration);
1910-
}
19111910
return NULL;
19121911
}
19131912

0 commit comments

Comments
 (0)