Skip to content

Commit 77c9681

Browse files
committed
Issue #25887: Raise a RuntimeError when a coroutine is awaited more than once.
1 parent b2a2aa7 commit 77c9681

File tree

4 files changed

+169
-12
lines changed

4 files changed

+169
-12
lines changed

Doc/reference/datamodel.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2316,6 +2316,10 @@ Coroutines also have the methods listed below, which are analogous to
23162316
those of generators (see :ref:`generator-methods`). However, unlike
23172317
generators, coroutines do not directly support iteration.
23182318

2319+
.. versionchanged:: 3.5.2
2320+
It is a :exc:`RuntimeError` to await on a coroutine more than once.
2321+
2322+
23192323
.. method:: coroutine.send(value)
23202324

23212325
Starts or resumes execution of the coroutine. If *value* is ``None``,

Lib/test/test_coroutines.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,147 @@ async def coro():
569569
"coroutine ignored GeneratorExit"):
570570
c.close()
571571

572+
def test_func_15(self):
573+
# See http://bugs.python.org/issue25887 for details
574+
575+
async def spammer():
576+
return 'spam'
577+
async def reader(coro):
578+
return await coro
579+
580+
spammer_coro = spammer()
581+
582+
with self.assertRaisesRegex(StopIteration, 'spam'):
583+
reader(spammer_coro).send(None)
584+
585+
with self.assertRaisesRegex(RuntimeError,
586+
'cannot reuse already awaited coroutine'):
587+
reader(spammer_coro).send(None)
588+
589+
def test_func_16(self):
590+
# See http://bugs.python.org/issue25887 for details
591+
592+
@types.coroutine
593+
def nop():
594+
yield
595+
async def send():
596+
await nop()
597+
return 'spam'
598+
async def read(coro):
599+
await nop()
600+
return await coro
601+
602+
spammer = send()
603+
604+
reader = read(spammer)
605+
reader.send(None)
606+
reader.send(None)
607+
with self.assertRaisesRegex(Exception, 'ham'):
608+
reader.throw(Exception('ham'))
609+
610+
reader = read(spammer)
611+
reader.send(None)
612+
with self.assertRaisesRegex(RuntimeError,
613+
'cannot reuse already awaited coroutine'):
614+
reader.send(None)
615+
616+
with self.assertRaisesRegex(RuntimeError,
617+
'cannot reuse already awaited coroutine'):
618+
reader.throw(Exception('wat'))
619+
620+
def test_func_17(self):
621+
# See http://bugs.python.org/issue25887 for details
622+
623+
async def coroutine():
624+
return 'spam'
625+
626+
coro = coroutine()
627+
with self.assertRaisesRegex(StopIteration, 'spam'):
628+
coro.send(None)
629+
630+
with self.assertRaisesRegex(RuntimeError,
631+
'cannot reuse already awaited coroutine'):
632+
coro.send(None)
633+
634+
with self.assertRaisesRegex(RuntimeError,
635+
'cannot reuse already awaited coroutine'):
636+
coro.throw(Exception('wat'))
637+
638+
# Closing a coroutine shouldn't raise any exception even if it's
639+
# already closed/exhausted (similar to generators)
640+
coro.close()
641+
coro.close()
642+
643+
def test_func_18(self):
644+
# See http://bugs.python.org/issue25887 for details
645+
646+
async def coroutine():
647+
return 'spam'
648+
649+
coro = coroutine()
650+
await_iter = coro.__await__()
651+
it = iter(await_iter)
652+
653+
with self.assertRaisesRegex(StopIteration, 'spam'):
654+
it.send(None)
655+
656+
with self.assertRaisesRegex(RuntimeError,
657+
'cannot reuse already awaited coroutine'):
658+
it.send(None)
659+
660+
with self.assertRaisesRegex(RuntimeError,
661+
'cannot reuse already awaited coroutine'):
662+
# Although the iterator protocol requires iterators to
663+
# raise another StopIteration here, we don't want to do
664+
# that. In this particular case, the iterator will raise
665+
# a RuntimeError, so that 'yield from' and 'await'
666+
# expressions will trigger the error, instead of silently
667+
# ignoring the call.
668+
next(it)
669+
670+
with self.assertRaisesRegex(RuntimeError,
671+
'cannot reuse already awaited coroutine'):
672+
it.throw(Exception('wat'))
673+
674+
with self.assertRaisesRegex(RuntimeError,
675+
'cannot reuse already awaited coroutine'):
676+
it.throw(Exception('wat'))
677+
678+
# Closing a coroutine shouldn't raise any exception even if it's
679+
# already closed/exhausted (similar to generators)
680+
it.close()
681+
it.close()
682+
683+
def test_func_19(self):
684+
CHK = 0
685+
686+
@types.coroutine
687+
def foo():
688+
nonlocal CHK
689+
yield
690+
try:
691+
yield
692+
except GeneratorExit:
693+
CHK += 1
694+
695+
async def coroutine():
696+
await foo()
697+
698+
coro = coroutine()
699+
700+
coro.send(None)
701+
coro.send(None)
702+
703+
self.assertEqual(CHK, 0)
704+
coro.close()
705+
self.assertEqual(CHK, 1)
706+
707+
for _ in range(3):
708+
# Closing a coroutine shouldn't raise any exception even if it's
709+
# already closed/exhausted (similar to generators)
710+
coro.close()
711+
self.assertEqual(CHK, 1)
712+
572713
def test_cr_await(self):
573714
@types.coroutine
574715
def a():

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ Core and Builtins
6969

7070
- Issue #25660: Fix TAB key behaviour in REPL with readline.
7171

72+
- Issue #25887: Raise a RuntimeError when a coroutine object is awaited
73+
more than once.
74+
7275

7376
Library
7477
-------

Objects/genobject.c

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ gen_dealloc(PyGenObject *gen)
7878
}
7979

8080
static PyObject *
81-
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
81+
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
8282
{
8383
PyThreadState *tstate = PyThreadState_GET();
8484
PyFrameObject *f = gen->gi_frame;
@@ -92,9 +92,18 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
9292
return NULL;
9393
}
9494
if (f == NULL || f->f_stacktop == NULL) {
95-
/* Only set exception if called from send() */
96-
if (arg && !exc)
95+
if (PyCoro_CheckExact(gen) && !closing) {
96+
/* `gen` is an exhausted coroutine: raise an error,
97+
except when called from gen_close(), which should
98+
always be a silent method. */
99+
PyErr_SetString(
100+
PyExc_RuntimeError,
101+
"cannot reuse already awaited coroutine");
102+
} else if (arg && !exc) {
103+
/* `gen` is an exhausted generator:
104+
only set exception if called from send(). */
97105
PyErr_SetNone(PyExc_StopIteration);
106+
}
98107
return NULL;
99108
}
100109

@@ -220,7 +229,7 @@ return next yielded value or raise StopIteration.");
220229
PyObject *
221230
_PyGen_Send(PyGenObject *gen, PyObject *arg)
222231
{
223-
return gen_send_ex(gen, arg, 0);
232+
return gen_send_ex(gen, arg, 0, 0);
224233
}
225234

226235
PyDoc_STRVAR(close_doc,
@@ -292,7 +301,7 @@ gen_close(PyGenObject *gen, PyObject *args)
292301
}
293302
if (err == 0)
294303
PyErr_SetNone(PyExc_GeneratorExit);
295-
retval = gen_send_ex(gen, Py_None, 1);
304+
retval = gen_send_ex(gen, Py_None, 1, 1);
296305
if (retval) {
297306
char *msg = "generator ignored GeneratorExit";
298307
if (PyCoro_CheckExact(gen))
@@ -336,7 +345,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
336345
gen->gi_running = 0;
337346
Py_DECREF(yf);
338347
if (err < 0)
339-
return gen_send_ex(gen, Py_None, 1);
348+
return gen_send_ex(gen, Py_None, 1, 0);
340349
goto throw_here;
341350
}
342351
if (PyGen_CheckExact(yf)) {
@@ -369,10 +378,10 @@ gen_throw(PyGenObject *gen, PyObject *args)
369378
/* Termination repetition of YIELD_FROM */
370379
gen->gi_frame->f_lasti++;
371380
if (_PyGen_FetchStopIterationValue(&val) == 0) {
372-
ret = gen_send_ex(gen, val, 0);
381+
ret = gen_send_ex(gen, val, 0, 0);
373382
Py_DECREF(val);
374383
} else {
375-
ret = gen_send_ex(gen, Py_None, 1);
384+
ret = gen_send_ex(gen, Py_None, 1, 0);
376385
}
377386
}
378387
return ret;
@@ -426,7 +435,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
426435
}
427436

428437
PyErr_Restore(typ, val, tb);
429-
return gen_send_ex(gen, Py_None, 1);
438+
return gen_send_ex(gen, Py_None, 1, 0);
430439

431440
failed_throw:
432441
/* Didn't use our arguments, so restore their original refcounts */
@@ -440,7 +449,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
440449
static PyObject *
441450
gen_iternext(PyGenObject *gen)
442451
{
443-
return gen_send_ex(gen, NULL, 0);
452+
return gen_send_ex(gen, NULL, 0, 0);
444453
}
445454

446455
/*
@@ -901,13 +910,13 @@ coro_wrapper_dealloc(PyCoroWrapper *cw)
901910
static PyObject *
902911
coro_wrapper_iternext(PyCoroWrapper *cw)
903912
{
904-
return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0);
913+
return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0);
905914
}
906915

907916
static PyObject *
908917
coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg)
909918
{
910-
return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0);
919+
return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0);
911920
}
912921

913922
static PyObject *

0 commit comments

Comments
 (0)