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

(cherry picked from commit 991adca012f5e106c2d4040ce619c696ba6f9c46)

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

index f91e70aecbad94ce7d0c2558e62cb3881b47903e..d7867d128a8afe97fe7edcb63c5a8b609b04070b 100644 (file)
@@ -180,7 +180,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 4f05319457985ad91aeb4b5b49d76764a6886c70..5462c80ad3013a9a5aa9dd570f9d34f7a2a63895 100644 (file)
@@ -588,6 +588,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 58b8d262ecfee141cebd41811f58e1debdee3767..b6b405446f6c7e745cfbf3f4c10d5429e0947a3c 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -36,6 +36,9 @@ Core and Builtins
 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 a77ff9689e4705e64d608bcacd4b32aae473f202..75327fa3014be95a129d4cd11fd7b62766fd7c6f 100644 (file)
@@ -1984,6 +1984,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) {
@@ -2001,6 +2011,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);