https://github.com/python/cpython/commit/392ad7ed46a4cbaa4bfe0bdc4a38ccd2911a599d
commit: 392ad7ed46a4cbaa4bfe0bdc4a38ccd2911a599d
branch: main
author: Timofei Ivankov <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-09-11T12:50:35Z
summary:
gh-157213: Fix stale asyncio.gather() edges in the await graph (#157214)
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 8f4dce831c5ccce..3ef3a578d93ab49 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -772,6 +772,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.
@@ -905,6 +910,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 36841672e1f0f65..f0fda848927dd40 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 fa3eaef0ac03375..86d90359fa4e585 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 000000000000000..801a76c3f3e7ad6
--- /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]