diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 2bee5c050ded7d..faee0cec6b668d 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -109,6 +109,7 @@ def __init__(self, coro, *, loop=None, name=None): self._fut_waiter = None self._coro = coro self._context = contextvars.copy_context() + self.__cancel_requested__ = False self._loop.call_soon(self.__step, context=self._context) _register_task(self) @@ -201,6 +202,10 @@ def cancel(self, msg=None): self._log_traceback = False if self.done(): return False + if self.__cancel_requested__: + # Cancel was requested by previous task.cancel() call + return False + self.__cancel_requested__ = True if self._fut_waiter is not None: if self._fut_waiter.cancel(msg=msg): # Leave self._fut_waiter; it may be a Task that @@ -634,7 +639,7 @@ def _ensure_future(coro_or_future, *, loop=None): loop = events._get_event_loop(stacklevel=4) try: return loop.create_task(coro_or_future) - except RuntimeError: + except RuntimeError: if not called_wrap_awaitable: coro_or_future.close() raise diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 8c4dceacdeec96..b14240327d60c7 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -496,6 +496,25 @@ async def run(): # This also distinguishes from the initial has_cycle=None. self.assertEqual(has_cycle, False) + def test___cancel_requested__(self): + loop = asyncio.new_event_loop() + + async def task(): + await asyncio.sleep(10) + return 12 + + try: + t = self.new_task(loop, task()) + self.assertFalse(t.__cancel_requested__) + self.assertTrue(t.cancel()) + self.assertTrue(t.__cancel_requested__) + self.assertFalse(t.cancel()) + + with self.assertRaises(asyncio.CancelledError): + loop.run_until_complete(t) + finally: + loop.close() + def test_cancel(self): def gen(): diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 72dbdb8902f121..ef176b6bc596a9 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -91,6 +91,7 @@ typedef struct { PyObject *task_context; int task_must_cancel; int task_log_destroy_pending; + int task_cancel_requested; } TaskObj; typedef struct { @@ -2039,6 +2040,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, Py_CLEAR(self->task_fut_waiter); self->task_must_cancel = 0; self->task_log_destroy_pending = 1; + self->task_cancel_requested = 0; Py_INCREF(coro); Py_XSETREF(self->task_coro, coro); @@ -2141,6 +2143,32 @@ TaskObj_get_fut_waiter(TaskObj *task, void *Py_UNUSED(ignored)) Py_RETURN_NONE; } +static PyObject * +TaskObj_get_cancel_requested(TaskObj *task, void *Py_UNUSED(ignored)) +{ + if (task->task_cancel_requested) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } +} + +static int +TaskObj_set_cancel_requested(TaskObj *task, PyObject *val, void *Py_UNUSED(ignored)) +{ + if (val == NULL) { + PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); + return -1; + } + int is_true = PyObject_IsTrue(val); + if (is_true < 0) { + return -1; + } + task->task_cancel_requested = is_true; + return 0; +} + /*[clinic input] _asyncio.Task._make_cancelled_error @@ -2205,6 +2233,11 @@ _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg) Py_RETURN_FALSE; } + if (self->task_cancel_requested) { + Py_RETURN_FALSE; + } + self->task_cancel_requested = 1; + if (self->task_fut_waiter) { PyObject *res; int is_true; @@ -2473,6 +2506,8 @@ static PyGetSetDef TaskType_getsetlist[] = { {"_must_cancel", (getter)TaskObj_get_must_cancel, NULL, NULL}, {"_coro", (getter)TaskObj_get_coro, NULL, NULL}, {"_fut_waiter", (getter)TaskObj_get_fut_waiter, NULL, NULL}, + {"__cancel_requested__", (getter)TaskObj_get_cancel_requested, + (setter)TaskObj_set_cancel_requested, NULL}, {NULL} /* Sentinel */ };