From 863b6749093a86810c4077112a857363410cc221 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 29 May 2018 17:20:02 -0400 Subject: [PATCH] bpo-32684: Fix gather to propagate cancel of itself with return_exceptions (GH-7209) --- Doc/library/asyncio-task.rst | 4 +++ Lib/asyncio/tasks.py | 14 ++++++++- Lib/test/test_asyncio/test_tasks.py | 29 ++++++++++++++++++- .../2018-05-29-12-51-18.bpo-32684.ZEIism.rst | 1 + 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-05-29-12-51-18.bpo-32684.ZEIism.rst diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 233cc94549..dc450c375a 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -640,6 +640,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.7.0 + 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 67fb57c6a7..6cef33d521 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -591,6 +591,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(): @@ -599,6 +600,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 @@ -673,7 +679,13 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): res = fut.result() results.append(res) - outer.set_result(results) + if outer._cancel_requested: + # If gather is being cancelled we must propagate the + # cancellation regardless of *return_exceptions* argument. + # See issue 32684. + outer.set_exception(futures.CancelledError()) + else: + outer.set_result(results) arg_to_fut = {} children = [] diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 33300c91a3..1280584d31 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2037,7 +2037,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() @@ -2067,6 +2067,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), + return_exceptions=True, + loop=loop) + if time > 1: + return + + async def main(): + qwe = asyncio.Task(test()) + await asyncio.sleep(0.2) + 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 0000000000..b360bbcf79 --- /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. -- 2.40.0