mirror of https://github.com/python/cpython
Issue #25887: Raise a RuntimeError when a coroutine is awaited more than once.
This commit is contained in:
parent
b2a2aa7664
commit
77c96813ab
|
@ -2316,6 +2316,10 @@ Coroutines also have the methods listed below, which are analogous to
|
||||||
those of generators (see :ref:`generator-methods`). However, unlike
|
those of generators (see :ref:`generator-methods`). However, unlike
|
||||||
generators, coroutines do not directly support iteration.
|
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)
|
.. method:: coroutine.send(value)
|
||||||
|
|
||||||
Starts or resumes execution of the coroutine. If *value* is ``None``,
|
Starts or resumes execution of the coroutine. If *value* is ``None``,
|
||||||
|
|
|
@ -569,6 +569,147 @@ class CoroutineTest(unittest.TestCase):
|
||||||
"coroutine ignored GeneratorExit"):
|
"coroutine ignored GeneratorExit"):
|
||||||
c.close()
|
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):
|
def test_cr_await(self):
|
||||||
@types.coroutine
|
@types.coroutine
|
||||||
def a():
|
def a():
|
||||||
|
|
|
@ -69,6 +69,9 @@ Core and Builtins
|
||||||
|
|
||||||
- Issue #25660: Fix TAB key behaviour in REPL with readline.
|
- 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
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -78,7 +78,7 @@ gen_dealloc(PyGenObject *gen)
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
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();
|
PyThreadState *tstate = PyThreadState_GET();
|
||||||
PyFrameObject *f = gen->gi_frame;
|
PyFrameObject *f = gen->gi_frame;
|
||||||
|
@ -92,9 +92,18 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (f == NULL || f->f_stacktop == NULL) {
|
if (f == NULL || f->f_stacktop == NULL) {
|
||||||
/* Only set exception if called from send() */
|
if (PyCoro_CheckExact(gen) && !closing) {
|
||||||
if (arg && !exc)
|
/* `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);
|
PyErr_SetNone(PyExc_StopIteration);
|
||||||
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +229,7 @@ return next yielded value or raise StopIteration.");
|
||||||
PyObject *
|
PyObject *
|
||||||
_PyGen_Send(PyGenObject *gen, PyObject *arg)
|
_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,
|
PyDoc_STRVAR(close_doc,
|
||||||
|
@ -292,7 +301,7 @@ gen_close(PyGenObject *gen, PyObject *args)
|
||||||
}
|
}
|
||||||
if (err == 0)
|
if (err == 0)
|
||||||
PyErr_SetNone(PyExc_GeneratorExit);
|
PyErr_SetNone(PyExc_GeneratorExit);
|
||||||
retval = gen_send_ex(gen, Py_None, 1);
|
retval = gen_send_ex(gen, Py_None, 1, 1);
|
||||||
if (retval) {
|
if (retval) {
|
||||||
char *msg = "generator ignored GeneratorExit";
|
char *msg = "generator ignored GeneratorExit";
|
||||||
if (PyCoro_CheckExact(gen))
|
if (PyCoro_CheckExact(gen))
|
||||||
|
@ -336,7 +345,7 @@ gen_throw(PyGenObject *gen, PyObject *args)
|
||||||
gen->gi_running = 0;
|
gen->gi_running = 0;
|
||||||
Py_DECREF(yf);
|
Py_DECREF(yf);
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
return gen_send_ex(gen, Py_None, 1);
|
return gen_send_ex(gen, Py_None, 1, 0);
|
||||||
goto throw_here;
|
goto throw_here;
|
||||||
}
|
}
|
||||||
if (PyGen_CheckExact(yf)) {
|
if (PyGen_CheckExact(yf)) {
|
||||||
|
@ -369,10 +378,10 @@ gen_throw(PyGenObject *gen, PyObject *args)
|
||||||
/* Termination repetition of YIELD_FROM */
|
/* Termination repetition of YIELD_FROM */
|
||||||
gen->gi_frame->f_lasti++;
|
gen->gi_frame->f_lasti++;
|
||||||
if (_PyGen_FetchStopIterationValue(&val) == 0) {
|
if (_PyGen_FetchStopIterationValue(&val) == 0) {
|
||||||
ret = gen_send_ex(gen, val, 0);
|
ret = gen_send_ex(gen, val, 0, 0);
|
||||||
Py_DECREF(val);
|
Py_DECREF(val);
|
||||||
} else {
|
} else {
|
||||||
ret = gen_send_ex(gen, Py_None, 1);
|
ret = gen_send_ex(gen, Py_None, 1, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -426,7 +435,7 @@ throw_here:
|
||||||
}
|
}
|
||||||
|
|
||||||
PyErr_Restore(typ, val, tb);
|
PyErr_Restore(typ, val, tb);
|
||||||
return gen_send_ex(gen, Py_None, 1);
|
return gen_send_ex(gen, Py_None, 1, 0);
|
||||||
|
|
||||||
failed_throw:
|
failed_throw:
|
||||||
/* Didn't use our arguments, so restore their original refcounts */
|
/* Didn't use our arguments, so restore their original refcounts */
|
||||||
|
@ -440,7 +449,7 @@ failed_throw:
|
||||||
static PyObject *
|
static PyObject *
|
||||||
gen_iternext(PyGenObject *gen)
|
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 *
|
static PyObject *
|
||||||
coro_wrapper_iternext(PyCoroWrapper *cw)
|
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 *
|
static PyObject *
|
||||||
coro_wrapper_send(PyCoroWrapper *cw, PyObject *arg)
|
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 *
|
static PyObject *
|
||||||
|
|
Loading…
Reference in New Issue