Skip to content

Commit

Permalink
Fixed asyncio CancelScope not recognizing its own cancellation exce…
Browse files Browse the repository at this point in the history
…ption (#639)
  • Loading branch information
agronholm authored Nov 22, 2023
1 parent 523381a commit c360b99
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 18 deletions.
2 changes: 2 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
- Exposed the ``ResourceGuard`` class in the public API
- Fixed ``RuntimeError: Runner is closed`` when running higher-scoped async generator
fixtures in some cases (`#619 <https://github.com/agronholm/anyio/issues/619>`_)
- Fixed discrepancy between ``asyncio`` and ``trio`` where reraising a cancellation
exception in an ``except*`` block would incorrectly bubble out of its cancel scope

**4.0.0**

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ trio = ["trio >= 0.23"]
test = [
"anyio[trio]",
"coverage[toml] >= 7",
"exceptiongroup >= 1.2.0",
"hypothesis >= 4.0",
"psutil >= 5.9",
"pytest >= 7.0",
Expand Down
33 changes: 15 additions & 18 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from asyncio import run as native_run
from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined]
from collections import OrderedDict, deque
from collections.abc import AsyncIterator, Iterable
from collections.abc import AsyncIterator, Generator, Iterable
from concurrent.futures import Future
from contextlib import suppress
from contextvars import Context, copy_context
Expand Down Expand Up @@ -418,8 +418,13 @@ def __exit__(
if self._shield:
self._deliver_cancellation_to_parent()

if isinstance(exc_val, CancelledError) and self._cancel_called:
self._cancelled_caught = self._uncancel(exc_val)
if self._cancel_called and exc_val is not None:
for exc in iterate_exceptions(exc_val):
if isinstance(exc, CancelledError):
self._cancelled_caught = self._uncancel(exc)
if self._cancelled_caught:
break

return self._cancelled_caught

return None
Expand Down Expand Up @@ -604,22 +609,14 @@ def started(self, value: T_contra | None = None) -> None:
_task_states[task].parent_id = self._parent_id


def collapse_exception_group(excgroup: BaseExceptionGroup) -> BaseException:
exceptions = list(excgroup.exceptions)
modified = False
for i, exc in enumerate(exceptions):
if isinstance(exc, BaseExceptionGroup):
new_exc = collapse_exception_group(exc)
if new_exc is not exc:
modified = True
exceptions[i] = new_exc

if len(exceptions) == 1:
return exceptions[0]
elif modified:
return excgroup.derive(exceptions)
def iterate_exceptions(
exception: BaseException
) -> Generator[BaseException, None, None]:
if isinstance(exception, BaseExceptionGroup):
for exc in exception.exceptions:
yield from iterate_exceptions(exc)
else:
return excgroup
yield exception


class TaskGroup(abc.TaskGroup):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any, NoReturn, cast

import pytest
from exceptiongroup import catch

import anyio
from anyio import (
Expand Down Expand Up @@ -1282,6 +1283,16 @@ async def test_cancel_before_entering_task_group() -> None:
pytest.fail("This should not raise a cancellation exception")


async def test_reraise_cancelled_in_excgroup() -> None:
def handler(excgrp: BaseExceptionGroup) -> None:
raise

with CancelScope() as scope:
scope.cancel()
with catch({get_cancelled_exc_class(): handler}):
await anyio.sleep_forever()


class TestTaskStatusTyping:
"""
These tests do not do anything at run time, but since the test suite is also checked
Expand Down

0 comments on commit c360b99

Please sign in to comment.