From 844e08924703616abe19a493162f93fcfdaf5ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Wed, 22 Nov 2023 17:07:06 +0200 Subject: [PATCH] Fixed asyncio `CancelScope` not recognizing its own cancellation exception ...if it comes wrapped in an exception group. Fixes #634. --- docs/versionhistory.rst | 2 ++ pyproject.toml | 1 + src/anyio/_backends/_asyncio.py | 14 ++++++++++++-- tests/test_taskgroups.py | 11 +++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 906babfc..2419ddec 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -12,6 +12,8 @@ This library adheres to `Semantic Versioning 2.0 `_. - Removed a checkpoint when exiting a task group - Bumped minimum version of trio to v0.23 - Exposed the ``ResourceGuard`` class in the public API +- 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** diff --git a/pyproject.toml b/pyproject.toml index 7c8bf99f..26839eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index b27a538a..bc9b5ecf 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -418,8 +418,18 @@ 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: + if isinstance(exc_val, CancelledError): + self._cancelled_caught = self._uncancel(exc_val) + elif isinstance(exc_val, BaseExceptionGroup) and ( + excgrp := exc_val.split(CancelledError)[0] + ): + for exc in excgrp.exceptions: + if isinstance(exc, CancelledError): + self._cancelled_caught = self._uncancel(exc) + if self._cancelled_caught: + break + return self._cancelled_caught return None diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index bc227603..221f00f4 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -8,6 +8,7 @@ from typing import Any, NoReturn, cast import pytest +from exceptiongroup import catch import anyio from anyio import ( @@ -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