Things to check first
AnyIO version
master (d68670b)
Python version
CPython 3.13.12
What happened?
On the asyncio backend, CancelScope.__exit__ doesn't filter/swallow a level-cancellation exception that it should if the cancellation exception is in a group. The cancellation exception instead propagates and causes the run() to crash and raise the level-cancellation exception (as part of a group).
How can we reproduce the bug?
Minimal reproducers that fail on asyncio and pass on Trio:
async def test_exception_group_filtering() -> None:
"""Test that CancelledErrors are filtered out of exception groups."""
body_exc = RuntimeError()
def check_body_exc(exc: RuntimeError, /) -> bool:
return exc is body_exc
with pytest.RaisesGroup(pytest.RaisesExc(RuntimeError, check=check_body_exc)):
with CancelScope() as cs:
cs.cancel()
try:
raise body_exc
except BaseException as exc:
exceptions = [exc]
try:
await checkpoint()
except BaseException as exc2:
exceptions.append(exc2)
else:
pytest.fail("Did not raise a cancellation exception")
try:
raise BaseExceptionGroup("", exceptions)
finally:
# Prevent reference cycles.
del exceptions
async def test_nested_exception_group_filtering() -> None:
"""Test that CancelledErrors are filtered out of nested exception groups."""
body_exc = RuntimeError()
def check_body_exc(exc: RuntimeError, /) -> bool:
return exc is body_exc
with pytest.RaisesGroup(
pytest.RaisesGroup(pytest.RaisesExc(RuntimeError, check=check_body_exc))
):
with CancelScope() as cs:
cs.cancel()
try:
raise body_exc
except BaseException as exc:
exceptions = [exc]
try:
await checkpoint()
except BaseException as exc2:
exceptions.append(exc2)
else:
pytest.fail("Did not raise a cancellation exception")
try:
raise BaseExceptionGroup("", (BaseExceptionGroup("", exceptions),))
finally:
# Prevent reference cycles.
del exceptions
These ^ were minimized from the following less-minimized reproducer:
from anyio import CancelScope
from anyio.abc import AsyncResource
import anyio
import anyio.lowlevel
from types import TracebackType
import logging
class SafeAsyncResource(AsyncResource):
# This is an AsyncResource that always wraps its exceptions in a group. This
# prevents exceptions from the body of the `async with` from getting mistakenly
# swallowed & discarded. See also https://github.com/python-trio/trio/issues/455 /
# https://github.com/python-trio/trio/issues/1559.
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
exceptions = list[BaseException]()
try:
if exc_val is not None:
exceptions.append(exc_val)
# aclose() can raise an arbitrary exception or a cancellation exception,
# both of which may get caught later. Therefore we must not allow such an
# exception to eat `exc_val`; we should never swallow `exc_val`. So we raise
# both.
try:
await self.aclose()
except BaseException as exc:
exceptions.append(exc)
finally:
if exceptions:
try:
raise BaseExceptionGroup(
"Exceptions from AsyncResource __aexit__", exceptions
)
finally:
del exceptions
class SomeSafeAsyncResource(SafeAsyncResource):
async def aclose(self) -> None:
await anyio.lowlevel.checkpoint()
async def main() -> None:
try:
with CancelScope() as cs:
async with SomeSafeAsyncResource():
cs.cancel()
raise RuntimeError(
"exception raised by user code running under `async with SomeSafeAsyncResource`"
)
except* RuntimeError:
# Swallow it.
# logging.getLogger(__name__).exception("appropriate message:")
pass
if __name__ == "__main__":
anyio.run(main, backend="asyncio")
# anyio.run(main, backend="trio")
print("run() exited cleanly")
Things to check first
I have searched the existing issues and didn't find my bug already reported there
I have checked that my bug is still present in the latest release
AnyIO version
master (d68670b)
Python version
CPython 3.13.12
What happened?
On the asyncio backend,
CancelScope.__exit__doesn't filter/swallow a level-cancellation exception that it should if the cancellation exception is in a group. The cancellation exception instead propagates and causes therun()to crash and raise the level-cancellation exception (as part of a group).How can we reproduce the bug?
Minimal reproducers that fail on asyncio and pass on Trio:
These ^ were minimized from the following less-minimized reproducer: