Skip to content

Commit 9186395

Browse files
njsmithmiss-islington
authored andcommitted
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: python-trio/trio#1396 https://bugs.python.org/issue39606 (cherry picked from commit 925dc7f) Co-authored-by: Nathaniel J. Smith <[email protected]>
1 parent 669981b commit 9186395

File tree

3 files changed

+41
-6
lines changed

3 files changed

+41
-6
lines changed

Lib/test/test_asyncgen.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ async def main():
11281128

11291129
self.assertEqual([], messages)
11301130

1131-
def test_async_gen_await_anext_twice(self):
1131+
def test_async_gen_await_same_anext_coro_twice(self):
11321132
async def async_iterate():
11331133
yield 1
11341134
yield 2
@@ -1147,7 +1147,7 @@ async def run():
11471147

11481148
self.loop.run_until_complete(run())
11491149

1150-
def test_async_gen_await_aclose_twice(self):
1150+
def test_async_gen_await_same_aclose_coro_twice(self):
11511151
async def async_iterate():
11521152
yield 1
11531153
yield 2
@@ -1164,6 +1164,32 @@ async def run():
11641164

11651165
self.loop.run_until_complete(run())
11661166

1167+
def test_async_gen_aclose_twice_with_different_coros(self):
1168+
# Regression test for https://bugs.python.org/issue39606
1169+
async def async_iterate():
1170+
yield 1
1171+
yield 2
1172+
1173+
async def run():
1174+
it = async_iterate()
1175+
await it.aclose()
1176+
await it.aclose()
1177+
1178+
self.loop.run_until_complete(run())
1179+
1180+
def test_async_gen_aclose_after_exhaustion(self):
1181+
# Regression test for https://bugs.python.org/issue39606
1182+
async def async_iterate():
1183+
yield 1
1184+
yield 2
1185+
1186+
async def run():
1187+
it = async_iterate()
1188+
async for _ in it:
1189+
pass
1190+
await it.aclose()
1191+
1192+
self.loop.run_until_complete(run())
11671193

11681194
if __name__ == "__main__":
11691195
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix regression caused by fix for bpo-39386, that prevented calling
2+
``aclose`` on an async generator that had already been closed or exhausted.

Objects/genobject.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,16 +1812,22 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
18121812
PyFrameObject *f = gen->gi_frame;
18131813
PyObject *retval;
18141814

1815-
if (f == NULL || f->f_stacktop == NULL ||
1816-
o->agt_state == AWAITABLE_STATE_CLOSED) {
1815+
if (o->agt_state == AWAITABLE_STATE_CLOSED) {
18171816
PyErr_SetString(
18181817
PyExc_RuntimeError,
18191818
"cannot reuse already awaited aclose()/athrow()");
18201819
return NULL;
18211820
}
18221821

1822+
if (f == NULL || f->f_stacktop == NULL) {
1823+
o->agt_state = AWAITABLE_STATE_CLOSED;
1824+
PyErr_SetNone(PyExc_StopIteration);
1825+
return NULL;
1826+
}
1827+
18231828
if (o->agt_state == AWAITABLE_STATE_INIT) {
18241829
if (o->agt_gen->ag_running_async) {
1830+
o->agt_state = AWAITABLE_STATE_CLOSED;
18251831
if (o->agt_args == NULL) {
18261832
PyErr_SetString(
18271833
PyExc_RuntimeError,
@@ -1893,7 +1899,6 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
18931899
/* aclose() mode */
18941900
if (retval) {
18951901
if (_PyAsyncGenWrappedValue_CheckExact(retval)) {
1896-
o->agt_gen->ag_running_async = 0;
18971902
Py_DECREF(retval);
18981903
goto yield_close;
18991904
}
@@ -1908,16 +1913,17 @@ async_gen_athrow_send(PyAsyncGenAThrow *o, PyObject *arg)
19081913

19091914
yield_close:
19101915
o->agt_gen->ag_running_async = 0;
1916+
o->agt_state = AWAITABLE_STATE_CLOSED;
19111917
PyErr_SetString(
19121918
PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
19131919
return NULL;
19141920

19151921
check_error:
19161922
o->agt_gen->ag_running_async = 0;
1923+
o->agt_state = AWAITABLE_STATE_CLOSED;
19171924
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
19181925
PyErr_ExceptionMatches(PyExc_GeneratorExit))
19191926
{
1920-
o->agt_state = AWAITABLE_STATE_CLOSED;
19211927
if (o->agt_args == NULL) {
19221928
/* when aclose() is called we don't want to propagate
19231929
StopAsyncIteration or GeneratorExit; just raise
@@ -1951,6 +1957,7 @@ async_gen_athrow_throw(PyAsyncGenAThrow *o, PyObject *args)
19511957
/* aclose() mode */
19521958
if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) {
19531959
o->agt_gen->ag_running_async = 0;
1960+
o->agt_state = AWAITABLE_STATE_CLOSED;
19541961
Py_DECREF(retval);
19551962
PyErr_SetString(PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
19561963
return NULL;

0 commit comments

Comments
 (0)