Summary
Promise._fully_unpack_unsafe() carries a comment (promising/promise.py:820-833) claiming that cancelling a Promise whose first unpacking step already produced a nested Promise cancels only the wrapper:
# Note: cancelling this Promise does NOT propagate cancellation
# into the nested Promise being awaited below - asyncio's
# task-cancellation lands on this task and unwinds upward; the
# inner Promise's own task keeps running independently.
This does not match asyncio's actual behavior. The nested Promise IS dragged down: it ends up in _CANCELLED_BEFORE_UNPACKED_ONCE instead of running to completion.
Mechanism
When the outer Promise's full-unpacking task is suspended on result = await result (promising/promise.py:834-835), the task's _fut_waiter is the inner Promise's unpacking task (awaiting a Promise ultimately suspends on its task). Task.cancel() on the outer's task therefore calls cancel() on the _fut_waiter - i.e. on the inner Promise's task - so the cancellation propagates downward into the nested Promise, not just upward through the wrapper.
Observed failure
tests/race_conditions/test_cancellation_races.py::test_cancel_after_first_unpacking_leaves_nested_promise_running pins the isolation semantics the comment claims, and currently fails:
assert not inner.done()
AssertionError: assert not True
+ where True = done()
+ where done = <'..._gated_inner' Promise id=...>._CANCELLED_BEFORE_UNPACKED_ONCE.done
The parts of that test covering the outer promise's post-cancellation readability (unpacked_once() stays True, intermediate_promise() returns the nested Promise instead of raising) pass today.
What needs to happen
This is the TODO [CANCELLATION] design question at promising/promise.py:824-833, and the refactoring must resolve it explicitly - either:
- Make the isolation real - shield the nested
await (e.g. asyncio.shield-style) so cancelling the wrapper leaves the pass-through Promise's own task running independently. The TODO's own reasoning leans this way: a Promise that merely "passes through" another Promise didn't originate it, and shouldn't decide its fate. Keep the current test assertions.
- Embrace propagation - declare that cancelling a wrapper cancels the nested Promise it is currently awaiting, update the comment and docs to say so, and flip the test's assertions.
The decision should also spell out how this interacts with hierarchical (parent/child) cancellation, which the same TODO block flags as a separate relationship from "promise returned by another promise".
Summary
Promise._fully_unpack_unsafe()carries a comment (promising/promise.py:820-833) claiming that cancelling a Promise whose first unpacking step already produced a nested Promise cancels only the wrapper:This does not match asyncio's actual behavior. The nested Promise IS dragged down: it ends up in
_CANCELLED_BEFORE_UNPACKED_ONCEinstead of running to completion.Mechanism
When the outer Promise's full-unpacking task is suspended on
result = await result(promising/promise.py:834-835), the task's_fut_waiteris the inner Promise's unpacking task (awaiting a Promise ultimately suspends on its task).Task.cancel()on the outer's task therefore callscancel()on the_fut_waiter- i.e. on the inner Promise's task - so the cancellation propagates downward into the nested Promise, not just upward through the wrapper.Observed failure
tests/race_conditions/test_cancellation_races.py::test_cancel_after_first_unpacking_leaves_nested_promise_runningpins the isolation semantics the comment claims, and currently fails:The parts of that test covering the outer promise's post-cancellation readability (
unpacked_once()stays True,intermediate_promise()returns the nested Promise instead of raising) pass today.What needs to happen
This is the
TODO [CANCELLATION]design question atpromising/promise.py:824-833, and the refactoring must resolve it explicitly - either:await(e.g.asyncio.shield-style) so cancelling the wrapper leaves the pass-through Promise's own task running independently. The TODO's own reasoning leans this way: a Promise that merely "passes through" another Promise didn't originate it, and shouldn't decide its fate. Keep the current test assertions.The decision should also spell out how this interacts with hierarchical (parent/child) cancellation, which the same TODO block flags as a separate relationship from "promise returned by another promise".