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

(cherry picked from commit 52698c7ad9)

Co-authored-by: Yury Selivanov <yury@magic.io>
This commit is contained in:
Miss Islington (bot) 2020-08-14 02:44:00 -07:00 committed by GitHub
parent f3b6f3cd9a
commit cf79cbf447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 9 deletions

View File

@ -186,7 +186,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:

View 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

View 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

View File

@ -0,0 +1 @@
Fix asynchronous generators to handle GeneratorExit in athrow() correctly

View File

@ -1893,21 +1893,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;
}