bpo-39606: allow closing async generators that are already closed (GH-18475)

The fix for [bpo-39386](https://bugs.python.org/issue39386) attempted to make it so you couldn't reuse a
agen.aclose() coroutine object. It accidentally also prevented you
from calling aclose() at all on an async generator that was already
closed or exhausted. This commit fixes it so we're only blocking the
actually illegal cases, while allowing the legal cases.

The new tests failed before this patch. Also confirmed that this fixes
the test failures we were seeing in Trio with Python dev builds:
  https://github.com/python-trio/trio/pull/1396


https://bugs.python.org/issue39606
This commit is contained in:
Nathaniel J. Smith 2020-02-13 00:15:38 -08:00 committed by GitHub
parent 7514f4f625
commit 925dc7fb1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 41 additions and 6 deletions

View File

@ -1128,7 +1128,7 @@ class AsyncGenAsyncioTest(unittest.TestCase):
self.assertEqual([], messages) self.assertEqual([], messages)
def test_async_gen_await_anext_twice(self): def test_async_gen_await_same_anext_coro_twice(self):
async def async_iterate(): async def async_iterate():
yield 1 yield 1
yield 2 yield 2
@ -1147,7 +1147,7 @@ class AsyncGenAsyncioTest(unittest.TestCase):
self.loop.run_until_complete(run()) self.loop.run_until_complete(run())
def test_async_gen_await_aclose_twice(self): def test_async_gen_await_same_aclose_coro_twice(self):
async def async_iterate(): async def async_iterate():
yield 1 yield 1
yield 2 yield 2
@ -1164,6 +1164,32 @@ class AsyncGenAsyncioTest(unittest.TestCase):
self.loop.run_until_complete(run()) self.loop.run_until_complete(run())
def test_async_gen_aclose_twice_with_different_coros(self):
# Regression test for https://bugs.python.org/issue39606
async def async_iterate():
yield 1
yield 2
async def run():
it = async_iterate()
await it.aclose()
await it.aclose()
self.loop.run_until_complete(run())
def test_async_gen_aclose_after_exhaustion(self):
# Regression test for https://bugs.python.org/issue39606
async def async_iterate():
yield 1
yield 2
async def run():
it = async_iterate()
async for _ in it:
pass
await it.aclose()
self.loop.run_until_complete(run())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -0,0 +1,2 @@
Fix regression caused by fix for bpo-39386, that prevented calling
``aclose`` on an async generator that had already been closed or exhausted.

View File

@ -1797,16 +1797,22 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
PyFrameObject *f = gen->gi_frame; PyFrameObject *f = gen->gi_frame;
PyObject *retval; PyObject *retval;
if (f == NULL || f->f_stacktop == NULL || if (o->agt_state == AWAITABLE_STATE_CLOSED) {
o->agt_state == AWAITABLE_STATE_CLOSED) {
PyErr_SetString( PyErr_SetString(
PyExc_RuntimeError, PyExc_RuntimeError,
"cannot reuse already awaited aclose()/athrow()"); "cannot reuse already awaited aclose()/athrow()");
return NULL; return NULL;
} }
if (f == NULL || f->f_stacktop == NULL) {
o->agt_state = AWAITABLE_STATE_CLOSED;
PyErr_SetNone(PyExc_StopIteration);
return NULL;
}
if (o->agt_state == AWAITABLE_STATE_INIT) { if (o->agt_state == AWAITABLE_STATE_INIT) {
if (o->agt_gen->ag_running_async) { if (o->agt_gen->ag_running_async) {
o->agt_state = AWAITABLE_STATE_CLOSED;
if (o->agt_args == NULL) { if (o->agt_args == NULL) {
PyErr_SetString( PyErr_SetString(
PyExc_RuntimeError, PyExc_RuntimeError,
@ -1878,7 +1884,6 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
/* aclose() mode */ /* aclose() mode */
if (retval) { if (retval) {
if (_PyAsyncGenWrappedValue_CheckExact(retval)) { if (_PyAsyncGenWrappedValue_CheckExact(retval)) {
o->agt_gen->ag_running_async = 0;
Py_DECREF(retval); Py_DECREF(retval);
goto yield_close; goto yield_close;
} }
@ -1893,16 +1898,17 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
yield_close: yield_close:
o->agt_gen->ag_running_async = 0; o->agt_gen->ag_running_async = 0;
o->agt_state = AWAITABLE_STATE_CLOSED;
PyErr_SetString( PyErr_SetString(
PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG); PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
return NULL; return NULL;
check_error: check_error:
o->agt_gen->ag_running_async = 0; o->agt_gen->ag_running_async = 0;
o->agt_state = AWAITABLE_STATE_CLOSED;
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) || if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
PyErr_ExceptionMatches(PyExc_GeneratorExit)) PyErr_ExceptionMatches(PyExc_GeneratorExit))
{ {
o->agt_state = AWAITABLE_STATE_CLOSED;
if (o->agt_args == NULL) { if (o->agt_args == NULL) {
/* when aclose() is called we don't want to propagate /* when aclose() is called we don't want to propagate
StopAsyncIteration or GeneratorExit; just raise StopAsyncIteration or GeneratorExit; just raise
@ -1936,6 +1942,7 @@ async_gen_athrow_throw(PyAsyncGenAThrow *o, PyObject *args)
/* aclose() mode */ /* aclose() mode */
if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) { if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) {
o->agt_gen->ag_running_async = 0; o->agt_gen->ag_running_async = 0;
o->agt_state = AWAITABLE_STATE_CLOSED;
Py_DECREF(retval); Py_DECREF(retval);
PyErr_SetString(PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG); PyErr_SetString(PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
return NULL; return NULL;