diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 56ec0d0bee2..f1beadb6935 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2317,6 +2317,10 @@ Coroutines also have the methods listed below, which are analogous to those of generators (see :ref:`generator-methods`). However, unlike generators, coroutines do not directly support iteration. +.. versionchanged:: 3.5.2 + It is a :exc:`RuntimeError` to await on a coroutine more than once. + + .. method:: coroutine.send(value) Starts or resumes execution of the coroutine. If *value* is ``None``, diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 07c1cdf8483..954a9a1db2f 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -569,6 +569,147 @@ class CoroutineTest(unittest.TestCase): "coroutine ignored GeneratorExit"): c.close() + def test_func_15(self): + # See http://bugs.python.org/issue25887 for details + + async def spammer(): + return 'spam' + async def reader(coro): + return await coro + + spammer_coro = spammer() + + with self.assertRaisesRegex(StopIteration, 'spam'): + reader(spammer_coro).send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + reader(spammer_coro).send(None) + + def test_func_16(self): + # See http://bugs.python.org/issue25887 for details + + @types.coroutine + def nop(): + yield + async def send(): + await nop() + return 'spam' + async def read(coro): + await nop() + return await coro + + spammer = send() + + reader = read(spammer) + reader.send(None) + reader.send(None) + with self.assertRaisesRegex(Exception, 'ham'): + reader.throw(Exception('ham')) + + reader = read(spammer) + reader.send(None) + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + reader.send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + reader.throw(Exception('wat')) + + def test_func_17(self): + # See http://bugs.python.org/issue25887 for details + + async def coroutine(): + return 'spam' + + coro = coroutine() + with self.assertRaisesRegex(StopIteration, 'spam'): + coro.send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + coro.send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + coro.throw(Exception('wat')) + + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + coro.close() + coro.close() + + def test_func_18(self): + # See http://bugs.python.org/issue25887 for details + + async def coroutine(): + return 'spam' + + coro = coroutine() + await_iter = coro.__await__() + it = iter(await_iter) + + with self.assertRaisesRegex(StopIteration, 'spam'): + it.send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + it.send(None) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + # Although the iterator protocol requires iterators to + # raise another StopIteration here, we don't want to do + # that. In this particular case, the iterator will raise + # a RuntimeError, so that 'yield from' and 'await' + # expressions will trigger the error, instead of silently + # ignoring the call. + next(it) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + it.throw(Exception('wat')) + + with self.assertRaisesRegex(RuntimeError, + 'cannot reuse already awaited coroutine'): + it.throw(Exception('wat')) + + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + it.close() + it.close() + + def test_func_19(self): + CHK = 0 + + @types.coroutine + def foo(): + nonlocal CHK + yield + try: + yield + except GeneratorExit: + CHK += 1 + + async def coroutine(): + await foo() + + coro = coroutine() + + coro.send(None) + coro.send(None) + + self.assertEqual(CHK, 0) + coro.close() + self.assertEqual(CHK, 1) + + for _ in range(3): + # Closing a coroutine shouldn't raise any exception even if it's + # already closed/exhausted (similar to generators) + coro.close() + self.assertEqual(CHK, 1) + def test_cr_await(self): @types.coroutine def a(): diff --git a/Misc/NEWS b/Misc/NEWS index d51c3f31327..895325e4fe1 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -180,6 +180,9 @@ Core and Builtins single-digit longs. Microbenchmarks show 2-2.5x improvement. Built-in 'divmod' function is now also ~10% faster. +- Issue #25887: Raise a RuntimeError when a coroutine object is awaited + more than once. + Library ------- diff --git a/Objects/genobject.c b/Objects/genobject.c index 8ab7cf1a41f..72d44c1de95 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -78,7 +78,7 @@ gen_dealloc(PyGenObject *gen) } static PyObject * -gen_send_ex(PyGenObject *gen, PyObject *arg, int exc) +gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing) { PyThreadState *tstate = PyThreadState_GET(); PyFrameObject *f = gen->gi_frame; @@ -92,9 +92,18 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc) return NULL; } if (f == NULL || f->f_stacktop == NULL) { - /* Only set exception if called from send() */ - if (arg && !exc) + if (PyCoro_CheckExact(gen) && !closing) { + /* `gen` is an exhausted coroutine: raise an error, + except when called from gen_close(), which should + always be a silent method. */ + PyErr_SetString( + PyExc_RuntimeError, + "cannot reuse already awaited coroutine"); + } else if (arg && !exc) { + /* `gen` is an exhausted generator: + only set exception if called from send(). */ PyErr_SetNone(PyExc_StopIteration); + } return NULL; } @@ -220,7 +229,7 @@ return next yielded value or raise StopIteration."); PyObject * _PyGen_Send(PyGenObject *gen, PyObject *arg) { - return gen_send_ex(gen, arg, 0); + return gen_send_ex(gen, arg, 0, 0); } PyDoc_STRVAR(close_doc, @@ -292,7 +301,7 @@ gen_close(PyGenObject *gen, PyObject *args) } if (err == 0) PyErr_SetNone(PyExc_GeneratorExit); - retval = gen_send_ex(gen, Py_None, 1); + retval = gen_send_ex(gen, Py_None, 1, 1); if (retval) { char *msg = "generator ignored GeneratorExit"; if (PyCoro_CheckExact(gen)) @@ -336,7 +345,7 @@ gen_throw(PyGenObject *gen, PyObject *args) gen->gi_running = 0; Py_DECREF(yf); if (err < 0) - return gen_send_ex(gen, Py_None, 1); + return gen_send_ex(gen, Py_None, 1, 0); goto throw_here; } if (PyGen_CheckExact(yf)) { @@ -369,10 +378,10 @@ gen_throw(PyGenObject *gen, PyObject *args) /* Termination repetition of YIELD_FROM */ gen->gi_frame->f_lasti++; if (_PyGen_FetchStopIterationValue(&val) == 0) { - ret = gen_send_ex(gen, val, 0); + ret = gen_send_ex(gen, val, 0, 0); Py_DECREF(val); } else { - ret = gen_send_ex(gen, Py_None, 1); + ret = gen_send_ex(gen, Py_None, 1, 0); } } return ret; @@ -426,7 +435,7 @@ throw_here: } PyErr_Restore(typ, val, tb); - return gen_send_ex(gen, Py_None, 1); + return gen_send_ex(gen, Py_None, 1, 0); failed_throw: /* Didn't use our arguments, so restore their original refcounts */ @@ -440,7 +449,7 @@ failed_throw: static PyObject * gen_iternext(PyGenObject *gen) { - return gen_send_ex(gen, NULL, 0); + return gen_send_ex(gen, NULL, 0, 0); } /* @@ -893,13 +902,13 @@ coro_wrapper_dealloc(PyCoroWrapper *cw) static PyObject * coro_wrapper_iternext(PyCoroWrapper *cw) { - return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0); + return gen_send_ex((PyGenObject *)cw->cw_coroutine, NULL, 0, 0); } static PyObject * coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg) { - return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0); + return gen_send_ex((PyGenObject *)cw->cw_coroutine, arg, 0, 0); } static PyObject *