Skip to content

Commit 3dc7c52

Browse files
authored
bpo-30048: asyncio: fix Task.cancel() was ignored. (GH-1546)
when there are no more `await` or `yield (from)` before return in coroutine, cancel was ignored. example: async def coro(): asyncio.Task.current_task().cancel() return 42 ... res = await coro() # should raise CancelledError (cherry picked from commit 991adca)
1 parent a4465a5 commit 3dc7c52

File tree

4 files changed

+39
-1
lines changed

4 files changed

+39
-1
lines changed

Lib/asyncio/tasks.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,12 @@ def _step(self, exc=None):
180180
else:
181181
result = coro.throw(exc)
182182
except StopIteration as exc:
183-
self.set_result(exc.value)
183+
if self._must_cancel:
184+
# Task is cancelled right before coro stops.
185+
self._must_cancel = False
186+
self.set_exception(futures.CancelledError())
187+
else:
188+
self.set_result(exc.value)
184189
except futures.CancelledError:
185190
super().cancel() # I.e., Future.cancel(self).
186191
except Exception as exc:

Lib/test/test_asyncio/test_tasks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,24 @@ def task():
588588
self.assertFalse(t._must_cancel) # White-box test.
589589
self.assertFalse(t.cancel())
590590

591+
def test_cancel_at_end(self):
592+
"""coroutine end right after task is cancelled"""
593+
loop = asyncio.new_event_loop()
594+
self.set_event_loop(loop)
595+
596+
@asyncio.coroutine
597+
def task():
598+
t.cancel()
599+
self.assertTrue(t._must_cancel) # White-box test.
600+
return 12
601+
602+
t = self.new_task(loop, task())
603+
self.assertRaises(
604+
asyncio.CancelledError, loop.run_until_complete, t)
605+
self.assertTrue(t.done())
606+
self.assertFalse(t._must_cancel) # White-box test.
607+
self.assertFalse(t.cancel())
608+
591609
def test_stop_while_run_in_complete(self):
592610

593611
def gen():

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Core and Builtins
3636
Library
3737
-------
3838

39+
- bpo-30048: Fixed ``Task.cancel()`` can be ignored when the task is
40+
running coroutine and the coroutine returned without any more ``await``.
41+
3942
- bpo-30298: Weaken the condition of deprecation warnings for inline modifiers.
4043
Now allowed several subsequential inline modifiers at the start of the
4144
pattern (e.g. ``'(?i)(?s)...'``). In verbose mode whitespaces and comments

Modules/_asynciomodule.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,6 +1984,16 @@ task_step_impl(TaskObj *task, PyObject *exc)
19841984
if (_PyGen_FetchStopIterationValue(&o) == 0) {
19851985
/* The error is StopIteration and that means that
19861986
the underlying coroutine has resolved */
1987+
if (task->task_must_cancel) {
1988+
// Task is cancelled right before coro stops.
1989+
Py_DECREF(o);
1990+
task->task_must_cancel = 0;
1991+
et = asyncio_CancelledError;
1992+
Py_INCREF(et);
1993+
ev = NULL;
1994+
tb = NULL;
1995+
goto set_exception;
1996+
}
19871997
PyObject *res = future_set_result((FutureObj*)task, o);
19881998
Py_DECREF(o);
19891999
if (res == NULL) {
@@ -2001,6 +2011,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
20012011

20022012
/* Some other exception; pop it and call Task.set_exception() */
20032013
PyErr_Fetch(&et, &ev, &tb);
2014+
2015+
set_exception:
20042016
assert(et);
20052017
if (!ev || !PyObject_TypeCheck(ev, (PyTypeObject *) et)) {
20062018
PyErr_NormalizeException(&et, &ev, &tb);

0 commit comments

Comments
 (0)