diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 5b801aaf8ec..c0383331c1e 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -568,6 +568,10 @@ Task functions outer Future is *not* cancelled in this case. (This is to prevent the cancellation of one child to cause other children to be cancelled.) + .. versionchanged:: 3.6.6 + If the *gather* itself is cancelled, the cancellation is propagated + regardless of *return_exceptions*. + .. function:: iscoroutine(obj) Return ``True`` if *obj* is a :ref:`coroutine object `, diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index a294dfbf5e5..4cd2c6ac6c7 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -548,6 +548,7 @@ class _GatheringFuture(futures.Future): def __init__(self, children, *, loop=None): super().__init__(loop=loop) self._children = children + self._cancel_requested = False def cancel(self): if self.done(): @@ -556,6 +557,11 @@ class _GatheringFuture(futures.Future): for child in self._children: if child.cancel(): ret = True + if ret: + # If any child tasks were actually cancelled, we should + # propagate the cancellation request regardless of + # *return_exceptions* argument. See issue 32684. + self._cancel_requested = True return ret @@ -636,7 +642,10 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): results[i] = res nfinished += 1 if nfinished == nchildren: - outer.set_result(results) + if outer._cancel_requested: + outer.set_exception(futures.CancelledError()) + else: + outer.set_result(results) for i, fut in enumerate(children): fut.add_done_callback(functools.partial(_done_callback, i)) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index f41160ba322..7c5187348bc 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1991,7 +1991,7 @@ class BaseTaskTests: def test_cancel_wait_for(self): self._test_cancel_wait_for(60.0) - def test_cancel_gather(self): + def test_cancel_gather_1(self): """Ensure that a gathering future refuses to be cancelled once all children are done""" loop = asyncio.new_event_loop() @@ -2021,6 +2021,33 @@ class BaseTaskTests: self.assertFalse(gather_task.cancelled()) self.assertEqual(gather_task.result(), [42]) + def test_cancel_gather_2(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + + async def test(): + time = 0 + while True: + time += 0.05 + await asyncio.gather(asyncio.sleep(0.05, loop=loop), + return_exceptions=True, + loop=loop) + if time > 1: + return + + async def main(): + qwe = self.new_task(loop, test()) + await asyncio.sleep(0.2, loop=loop) + qwe.cancel() + try: + await qwe + except asyncio.CancelledError: + pass + else: + self.fail('gather did not propagate the cancellation request') + + loop.run_until_complete(main()) + def test_exception_traceback(self): # See http://bugs.python.org/issue28843 diff --git a/Misc/NEWS.d/next/Library/2018-05-29-12-51-18.bpo-32684.ZEIism.rst b/Misc/NEWS.d/next/Library/2018-05-29-12-51-18.bpo-32684.ZEIism.rst new file mode 100644 index 00000000000..b360bbcf799 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-29-12-51-18.bpo-32684.ZEIism.rst @@ -0,0 +1 @@ +Fix gather to propagate cancellation of itself even with return_exceptions.