Skip to content

Fix CancelScope reuse not being detected on the asyncio backend - #1296

Open
jaideeppyne wants to merge 1 commit into
agronholm:masterfrom
jaideeppyne:fix-cancelscope-reuse
Open

Fix CancelScope reuse not being detected on the asyncio backend#1296
jaideeppyne wants to merge 1 commit into
agronholm:masterfrom
jaideeppyne:fix-cancelscope-reuse

Conversation

@jaideeppyne

Copy link
Copy Markdown
Contributor

Changes

CancelScope.__enter__ on the asyncio backend guards against reuse with if self._active:, but __exit__ sets _active = False. So the guard only catches re-entering a scope that is still active, and entering a scope again after it has exited slips through silently, even though the guard's own message promises that each scope may only be used for a single with block. The Trio backend raises RuntimeError here.

_cancel_called also survives the first block, so reusing a cancelled scope silently cancels the body of the second block:

import anyio

async def main():
    scope = anyio.CancelScope()
    with scope:
        scope.cancel()

    with scope:
        await anyio.sleep(0)
        print("this never runs on asyncio")

anyio.run(main)                   # prints nothing, no error
anyio.run(main, backend="trio")   # RuntimeError: Each CancelScope may only be used for a single 'with' block

I track entry with a separate _has_been_entered flag that is never reset, which is how TaskGroup already guards its own re-entry in the same module. Both backends now raise the same RuntimeError.

The two new tests fail on asyncio, asyncio+uvloop and asyncio+eager without the patch, and already passed on Trio.

I found this with an asyncio/Trio differential harness. AI-assisted, and I ran and reviewed all of it myself.

Checklist

  • You've added tests (in tests/) which would fail without your patch
  • You've updated the documentation (in docs/), in case of behavior changes or new features
  • You've added a new changelog entry (in docs/versionhistory.rst).

The guard in CancelScope.__enter__ tested _active, which __exit__ resets,
so it only caught re-entering a scope that was still active. Entering a
scope again after it had been exited was silently allowed, even though the
error message promises that each scope may only be used for a single
'with' block, and even though the Trio backend raises RuntimeError.

Because _cancel_called survives the first block, reusing a cancelled scope
silently cancelled the body of the second 'with' block.

Track entry with a separate _has_been_entered flag that is never reset,
mirroring how TaskGroup already guards its own re-entry.
@jaideeppyne
jaideeppyne force-pushed the fix-cancelscope-reuse branch 2 times, most recently from c202306 to a58e99c Compare August 31, 2026 13:42

@fallenmi fallenmi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified exact head a58e99cc38bfafb3ed5d44506f578dd70223f80b and the identical current merge tree at 51585c78dce4fa83598156b79ca6ecc224ecc6c6.

The separate _has_been_entered flag correctly enforces one-shot use without overloading _active, which still tracks active-scope exit bookkeeping. This closes both ordinary post-exit reuse and the cancelled-scope path that silently cancelled the second block, and brings the asyncio backend in line with Trio. The two backend-parametrized regressions cover both paths, and the changelog entry is present.

All 18 exact-head check runs, the check suite and workflow, and both commit statuses are successful. I found no blocking issue. This was a source/API review; I did not rerun the suite locally.

Disclosure: this review was prepared with Codex assistance; I verified the exact refs, merge tree, source changes, policy, interactions, and live CI before submission.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants