summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/reference/datamodel.rst4
-rw-r--r--Lib/test/test_coroutines.py141
-rw-r--r--Misc/NEWS3
-rw-r--r--Objects/genobject.c33
4 files changed, 169 insertions, 12 deletions
diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst
index 764c491..cf0f069 100644
--- a/Doc/reference/datamodel.rst
+++ b/Doc/reference/datamodel.rst
@@ -2316,6 +2316,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 07c1cdf..954a9a1 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 459361b..4243a05 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -69,6 +69,9 @@ Core and Builtins
- Issue #25660: Fix TAB key behaviour in REPL with readline.
+- 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 00ebbf1..8201977 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);
}
/*
@@ -901,13 +910,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 *