Skip to content

Commit 863b674

Browse files
authored
bpo-32684: Fix gather to propagate cancel of itself with return_exceptions (GH-7209)
1 parent 1cee216 commit 863b674

File tree

4 files changed

+46
-2
lines changed

4 files changed

+46
-2
lines changed

Doc/library/asyncio-task.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,10 @@ Task functions
640640
outer Future is *not* cancelled in this case. (This is to prevent the
641641
cancellation of one child to cause other children to be cancelled.)
642642

643+
.. versionchanged:: 3.7.0
644+
If the *gather* itself is cancelled, the cancellation is propagated
645+
regardless of *return_exceptions*.
646+
643647
.. function:: iscoroutine(obj)
644648

645649
Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`,

Lib/asyncio/tasks.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ class _GatheringFuture(futures.Future):
591591
def __init__(self, children, *, loop=None):
592592
super().__init__(loop=loop)
593593
self._children = children
594+
self._cancel_requested = False
594595

595596
def cancel(self):
596597
if self.done():
@@ -599,6 +600,11 @@ def cancel(self):
599600
for child in self._children:
600601
if child.cancel():
601602
ret = True
603+
if ret:
604+
# If any child tasks were actually cancelled, we should
605+
# propagate the cancellation request regardless of
606+
# *return_exceptions* argument. See issue 32684.
607+
self._cancel_requested = True
602608
return ret
603609

604610

@@ -673,7 +679,13 @@ def _done_callback(fut):
673679
res = fut.result()
674680
results.append(res)
675681

676-
outer.set_result(results)
682+
if outer._cancel_requested:
683+
# If gather is being cancelled we must propagate the
684+
# cancellation regardless of *return_exceptions* argument.
685+
# See issue 32684.
686+
outer.set_exception(futures.CancelledError())
687+
else:
688+
outer.set_result(results)
677689

678690
arg_to_fut = {}
679691
children = []

Lib/test/test_asyncio/test_tasks.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2037,7 +2037,7 @@ def test_cancel_blocking_wait_for(self):
20372037
def test_cancel_wait_for(self):
20382038
self._test_cancel_wait_for(60.0)
20392039

2040-
def test_cancel_gather(self):
2040+
def test_cancel_gather_1(self):
20412041
"""Ensure that a gathering future refuses to be cancelled once all
20422042
children are done"""
20432043
loop = asyncio.new_event_loop()
@@ -2067,6 +2067,33 @@ def cancelling_callback(_):
20672067
self.assertFalse(gather_task.cancelled())
20682068
self.assertEqual(gather_task.result(), [42])
20692069

2070+
def test_cancel_gather_2(self):
2071+
loop = asyncio.new_event_loop()
2072+
self.addCleanup(loop.close)
2073+
2074+
async def test():
2075+
time = 0
2076+
while True:
2077+
time += 0.05
2078+
await asyncio.gather(asyncio.sleep(0.05),
2079+
return_exceptions=True,
2080+
loop=loop)
2081+
if time > 1:
2082+
return
2083+
2084+
async def main():
2085+
qwe = asyncio.Task(test())
2086+
await asyncio.sleep(0.2)
2087+
qwe.cancel()
2088+
try:
2089+
await qwe
2090+
except asyncio.CancelledError:
2091+
pass
2092+
else:
2093+
self.fail('gather did not propagate the cancellation request')
2094+
2095+
loop.run_until_complete(main())
2096+
20702097
def test_exception_traceback(self):
20712098
# See http://bugs.python.org/issue28843
20722099

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix gather to propagate cancellation of itself even with return_exceptions.

0 commit comments

Comments
 (0)