diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 825e91f5594d98..058a2bfe3b86f6 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -844,6 +844,7 @@ def _done_callback(fut, cur_task=cur_task): # All futures are done; create a list of results # and set it to the 'outer' future. results = [] + cancelled_child = None for fut in children: if fut.cancelled(): @@ -853,6 +854,8 @@ def _done_callback(fut, cur_task=cur_task): # to 'results' instead of raising it, don't bother # setting __context__. This also lets us preserve # calling '_make_cancelled_error()' at most once. + if cancelled_child is None: + cancelled_child = fut res = exceptions.CancelledError( '' if fut._cancel_message is None else fut._cancel_message) @@ -863,10 +866,15 @@ def _done_callback(fut, cur_task=cur_task): results.append(res) if outer._cancel_requested: + # If one or more children were cancelled, raise the exception + # from the first of those encountered, otherwise use the last + # child. See issue gh-97907. + if cancelled_child is None: + cancelled_child = fut # If gather is being cancelled we must propagate the # cancellation regardless of *return_exceptions* argument. # See issue 32684. - exc = fut._make_cancelled_error() + exc = cancelled_child._make_cancelled_error() outer.set_exception(exc) else: outer.set_result(results) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 8d7f17334547b3..e6e3338612e80b 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2418,6 +2418,27 @@ async def main(): 'raised by inner task to the gather() caller.' ) + def test_cancel_gather_3(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + barrier = asyncio.Barrier(2) + + async def f1(): + await barrier.wait() + await asyncio.sleep(1) + + async def f2(): + return 42 + + async def main(): + gfut = asyncio.gather(f2(), f1(), f2(), return_exceptions=True) + await barrier.wait() + gfut.cancel("my message") + await gfut + + with self.assertRaisesRegex(asyncio.CancelledError, "my message"): + loop.run_until_complete(main()) + def test_exception_traceback(self): # See http://bugs.python.org/issue28843 diff --git a/Misc/NEWS.d/next/Library/2025-05-01-21-13-23.gh-issue-97907.MFyz5K.rst b/Misc/NEWS.d/next/Library/2025-05-01-21-13-23.gh-issue-97907.MFyz5K.rst new file mode 100644 index 00000000000000..cdc0b75d38373f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-01-21-13-23.gh-issue-97907.MFyz5K.rst @@ -0,0 +1,2 @@ +Ensure cancellation error is raised by :func:`asyncio.gather` using the error +from a cancelled child, if there is one.