diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 3ef3a578d93ab4..29c5d9af9b4029 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -543,6 +543,10 @@ async def _cancel_and_wait(fut): cb = functools.partial(_release_waiter, waiter) fut.add_done_callback(cb) + # gh-157058: awaiting the waiter leaves no edge on fut, add it here + cur_task = current_task() + futures.future_add_to_awaited_by(fut, cur_task) + try: fut.cancel() # We cannot wait on *fut* directly to make @@ -550,6 +554,7 @@ async def _cancel_and_wait(fut): await waiter finally: fut.remove_done_callback(cb) + futures.future_discard_from_awaited_by(fut, cur_task) class _AsCompletedIterator: diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index f0fda848927dd4..a442a346ff06d9 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -173,6 +173,33 @@ class FakeCoro: self.assertEqual(len(result.call_stack), 2) + async def test_stack_wait_for_non_positive_timeout(self): + # gh-157058: wait_for(fut, 0) must still record the waiter + cleanup = asyncio.Future() + + async def worker(): + try: + await asyncio.Future() + finally: + await cleanup + + async def probe(t): + await asyncio.wait_for(t, 0) + + t = asyncio.ensure_future(worker()) + p = asyncio.create_task(probe(t), name='probe') + for _ in range(5): + await asyncio.sleep(0) + + stack = capture_test_stack(fut=t) + + cleanup.set_result(None) + await asyncio.gather(p, t, return_exceptions=True) + + self.assertEqual(stack[0][2], [ + ['T', ['a _cancel_and_wait', 'a wait_for', 'a probe'], []], + ]) + async def test_stack_gather(self): stack_for_deep = None diff --git a/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst b/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst new file mode 100644 index 00000000000000..9122ae0919dcb1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst @@ -0,0 +1,2 @@ +:func:`asyncio.wait_for` with a non-positive timeout now records the waiter +in the call graph.