https://github.com/python/cpython/commit/71f37f5fe16385c3daba7b9389f5af7601814112 commit: 71f37f5fe16385c3daba7b9389f5af7601814112 branch: 3.15 author: Miss Islington (bot) <[email protected]> committer: hugovk <[email protected]> date: 2026-09-11T21:05:46+03:00 summary:
[3.15] gh-157213: Fix stale asyncio.gather() edges in the await graph (GH-157214) (#157312) Co-authored-by: Timofei Ivankov <[email protected]> files: A Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst M Lib/asyncio/tasks.py M Lib/test/test_asyncio/test_graph.py M Lib/test/test_asyncio/test_tasks.py diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index d105f0c8efed7b..e9738897c33c0f 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -775,6 +775,11 @@ def cancel(self, msg=None): return ret +def _discard_awaited_by(children, waiter, outer): + for fut in children: + futures.future_discard_from_awaited_by(fut, waiter) + + def gather(*coros_or_futures, return_exceptions=False): """Return a future aggregating results from the given coroutines/futures. @@ -908,6 +913,10 @@ def _done_callback(fut, cur_task=cur_task): children.append(fut) outer = _GatheringFuture(children, loop=loop) + if cur_task is not None: + # gh-157213: a child outliving gather() must lose the awaited-by edge + outer.add_done_callback( + functools.partial(_discard_awaited_by, children, cur_task)) # Run done callbacks after GatheringFuture created so any post-processing # can be performed at this point # optimization: in the special case that *all* futures finished eagerly, diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 08e62f6e5bf19a..ba02263dff0b24 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -202,6 +202,29 @@ async def main(): ] ]) + async def test_stack_gather_survivor(self): + # gh-157213: a child that outlives gather() must not be shown as awaited + + async def fail(): + raise ValueError + + async def survivor(): + await asyncio.Future() + + t = asyncio.create_task(survivor(), name='survivor') + with self.assertRaises(ValueError): + await asyncio.gather(t, fail()) + + self.assertEqual(capture_test_stack(fut=t)[0], [ + 'T<survivor>', + ['a survivor'], + [] + ]) + + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + async def test_stack_shield(self): stack_for_shield = None diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 48fb5139f52e5f..1102a2d8a1b12a 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1231,6 +1231,25 @@ async def coro(): self.loop.run_until_complete(self.new_task(self.loop, coro())) + def test_gather_discards_awaited_by_for_pending(self): + # gh-157213: a child outliving gather() must lose the awaited-by edge + async def fail(): + raise ValueError + + async def survivor(): + await asyncio.Future() + + async def coro(): + t = self.new_task(self.loop, survivor()) + with self.assertRaises(ValueError): + await asyncio.gather(t, fail()) + self.assertFalse(t._asyncio_awaited_by) + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + + self.loop.run_until_complete(self.new_task(self.loop, coro())) + def test_wait_really_done(self): # there is possibility that some tasks in the pending list # became done but their callbacks haven't all been called yet diff --git a/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst b/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst new file mode 100644 index 00000000000000..801a76c3f3e7ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.gather` leaving stale await-graph edges on children that +outlive it. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
