Commit 863b6749 authored by Yury Selivanov's avatar Yury Selivanov Committed by GitHub

bpo-32684: Fix gather to propagate cancel of itself with return_exceptions (GH-7209)

parent 1cee216c
...@@ -640,6 +640,10 @@ Task functions ...@@ -640,6 +640,10 @@ Task functions
outer Future is *not* cancelled in this case. (This is to prevent the outer Future is *not* cancelled in this case. (This is to prevent the
cancellation of one child to cause other children to be cancelled.) 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) .. function:: iscoroutine(obj)
Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`, Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`,
......
...@@ -591,6 +591,7 @@ class _GatheringFuture(futures.Future): ...@@ -591,6 +591,7 @@ class _GatheringFuture(futures.Future):
def __init__(self, children, *, loop=None): def __init__(self, children, *, loop=None):
super().__init__(loop=loop) super().__init__(loop=loop)
self._children = children self._children = children
self._cancel_requested = False
def cancel(self): def cancel(self):
if self.done(): if self.done():
...@@ -599,6 +600,11 @@ class _GatheringFuture(futures.Future): ...@@ -599,6 +600,11 @@ class _GatheringFuture(futures.Future):
for child in self._children: for child in self._children:
if child.cancel(): if child.cancel():
ret = True 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 return ret
...@@ -673,6 +679,12 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False): ...@@ -673,6 +679,12 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False):
res = fut.result() res = fut.result()
results.append(res) results.append(res)
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) outer.set_result(results)
arg_to_fut = {} arg_to_fut = {}
......
...@@ -2037,7 +2037,7 @@ class BaseTaskTests: ...@@ -2037,7 +2037,7 @@ class BaseTaskTests:
def test_cancel_wait_for(self): def test_cancel_wait_for(self):
self._test_cancel_wait_for(60.0) 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 """Ensure that a gathering future refuses to be cancelled once all
children are done""" children are done"""
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
...@@ -2067,6 +2067,33 @@ class BaseTaskTests: ...@@ -2067,6 +2067,33 @@ class BaseTaskTests:
self.assertFalse(gather_task.cancelled()) self.assertFalse(gather_task.cancelled())
self.assertEqual(gather_task.result(), [42]) 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): def test_exception_traceback(self):
# See http://bugs.python.org/issue28843 # See http://bugs.python.org/issue28843
......
Fix gather to propagate cancellation of itself even with return_exceptions.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment