bpo-33786: Fix asynchronous generators to handle GeneratorExit in ath… · python/cpython@cf79cbf (original) (raw)
File tree
5 files changed
lines changed
- Misc/NEWS.d/next/Core and Builtins
5 files changed
lines changed
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -186,7 +186,7 @@ async def __aexit__(self, typ, value, traceback): | ||
186 | 186 | # in this implementation |
187 | 187 | try: |
188 | 188 | 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()") | |
190 | 190 | except StopAsyncIteration as exc: |
191 | 191 | return exc is not value |
192 | 192 | except RuntimeError as exc: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -108,6 +108,31 @@ def sync_iterate(g): | ||
108 | 108 | res.append(str(type(ex))) |
109 | 109 | return res |
110 | 110 | |
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 | + | |
111 | 136 | def async_iterate(g): |
112 | 137 | res = [] |
113 | 138 | while True: |
@@ -297,6 +322,37 @@ async def gen(): | ||
297 | 322 | "non-None value .* async generator"): |
298 | 323 | gen().__anext__().send(100) |
299 | 324 | |
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 | + | |
300 | 356 | def test_async_gen_api_01(self): |
301 | 357 | async def gen(): |
302 | 358 | yield 123 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -36,6 +36,28 @@ async def __aexit__(self, *args): | ||
36 | 36 | async with manager as context: |
37 | 37 | self.assertIs(manager, context) |
38 | 38 | |
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 | + | |
39 | 61 | def test_exit_is_abstract(self): |
40 | 62 | class MissingAexit(AbstractAsyncContextManager): |
41 | 63 | pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1 | +Fix asynchronous generators to handle GeneratorExit in athrow() correctly |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1893,21 +1893,20 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg) | ||
1893 | 1893 | return NULL; |
1894 | 1894 | |
1895 | 1895 | check_error: |
1896 | -if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) { | |
1896 | +if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) | | |
1897 | +PyErr_ExceptionMatches(PyExc_GeneratorExit)) | |
1898 | + { | |
1897 | 1899 | o->agt_state = AWAITABLE_STATE_CLOSED; |
1898 | 1900 | if (o->agt_args == NULL) { |
1899 | 1901 | /* 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 | + */ | |
1902 | 1906 | PyErr_Clear(); |
1903 | 1907 | PyErr_SetNone(PyExc_StopIteration); |
1904 | 1908 | } |
1905 | 1909 | } |
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 | - } | |
1911 | 1910 | return NULL; |
1912 | 1911 | } |
1913 | 1912 |