]> granicus.if.org Git - python/commitdiff
bpo-30048: asyncio: fix Task.cancel() was ignored. (GH-1097)
authorINADA Naoki <methane@users.noreply.github.com>
Thu, 11 May 2017 12:18:38 +0000 (21:18 +0900)
committerGitHub <noreply@github.com>
Thu, 11 May 2017 12:18:38 +0000 (21:18 +0900)
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

Lib/asyncio/tasks.py
Lib/test/test_asyncio/test_tasks.py
Misc/NEWS
Modules/_asynciomodule.c

index 4fbcf3ed2e944777e7baa4f48534f4c6bc6f8ba0..e4533000e7985f3a429e4a699ebacc79105e944a 100644 (file)
@@ -176,7 +176,12 @@ class Task(futures.Future):
             else:
                 result = coro.throw(exc)
         except StopIteration as exc:
-            self.set_result(exc.value)
+            if self._must_cancel:
+                # Task is cancelled right before coro stops.
+                self._must_cancel = False
+                self.set_exception(futures.CancelledError())
+            else:
+                self.set_result(exc.value)
         except futures.CancelledError:
             super().cancel()  # I.e., Future.cancel(self).
         except Exception as exc:
index 754a67519ba9d5b7c5f2b7a30fa6c7e47a595432..988b2885c30cd93362ffe1407c92cd54abcc070a 100644 (file)
@@ -587,6 +587,24 @@ class BaseTaskTests:
         self.assertFalse(t._must_cancel)  # White-box test.
         self.assertFalse(t.cancel())
 
+    def test_cancel_at_end(self):
+        """coroutine end right after task is cancelled"""
+        loop = asyncio.new_event_loop()
+        self.set_event_loop(loop)
+
+        @asyncio.coroutine
+        def task():
+            t.cancel()
+            self.assertTrue(t._must_cancel)  # White-box test.
+            return 12
+
+        t = self.new_task(loop, task())
+        self.assertRaises(
+            asyncio.CancelledError, loop.run_until_complete, t)
+        self.assertTrue(t.done())
+        self.assertFalse(t._must_cancel)  # White-box test.
+        self.assertFalse(t.cancel())
+
     def test_stop_while_run_in_complete(self):
 
         def gen():
index 5ee7ea65ff1df5a86f4c1968302fe25e41a9a240..4e17a66bd9624e4d4c402ca04edc1a4af07f2152 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -320,6 +320,9 @@ Extension Modules
 Library
 -------
 
+- bpo-30048: Fixed ``Task.cancel()`` can be ignored when the task is
+  running coroutine and the coroutine returned without any more ``await``.
+
 - bpo-30298: Weaken the condition of deprecation warnings for inline modifiers.
   Now allowed several subsequential inline modifiers at the start of the
   pattern (e.g. ``'(?i)(?s)...'``).  In verbose mode whitespaces and comments
index e902c041e50587ee199c6cabb3cc35e008568553..150ca198d27cfcdbb4bdf9b0b0c58e786993b534 100644 (file)
@@ -1985,6 +1985,16 @@ task_step_impl(TaskObj *task, PyObject *exc)
         if (_PyGen_FetchStopIterationValue(&o) == 0) {
             /* The error is StopIteration and that means that
                the underlying coroutine has resolved */
+            if (task->task_must_cancel) {
+                // Task is cancelled right before coro stops.
+                Py_DECREF(o);
+                task->task_must_cancel = 0;
+                et = asyncio_CancelledError;
+                Py_INCREF(et);
+                ev = NULL;
+                tb = NULL;
+                goto set_exception;
+            }
             PyObject *res = future_set_result((FutureObj*)task, o);
             Py_DECREF(o);
             if (res == NULL) {
@@ -2002,6 +2012,8 @@ task_step_impl(TaskObj *task, PyObject *exc)
 
         /* Some other exception; pop it and call Task.set_exception() */
         PyErr_Fetch(&et, &ev, &tb);
+
+set_exception:
         assert(et);
         if (!ev || !PyObject_TypeCheck(ev, (PyTypeObject *) et)) {
             PyErr_NormalizeException(&et, &ev, &tb);