From ceaf1ab4022f4783277563581fa09121709fdd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 14 Apr 2024 22:27:44 +0300 Subject: [PATCH] Fixed cancellation delivery sometimes incrementing the cancel count of the wrong cancel scope Fixes #716. --- docs/versionhistory.rst | 3 +++ src/anyio/_backends/_asyncio.py | 2 +- tests/test_taskgroups.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index fb81ec05..bdf0922a 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -12,6 +12,9 @@ This library adheres to `Semantic Versioning 2.0 `_. when cancelling a task in a TaskGroup created with the ``start()`` method before the first checkpoint is reached after calling ``task_status.started()`` (`#706 `_; PR by Dominik Schwabe) +- Fixed cancellation delivery on asyncio incrementing the wrong cancel scope's + cancellation counter when cascading a cancel operation to a child scope, thus failing + to uncancel the host task (`#716 `_) - Fixed erroneous ``TypedAttributeLookupError`` if a typed attribute getter raises ``KeyError`` - Fixed the asyncio backend not respecting the ``PYTHONASYNCIODEBUG`` environment diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 860c9999..ecd865a7 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -488,7 +488,7 @@ def _deliver_cancellation(self, origin: CancelScope) -> bool: if task is not current and (task is self._host_task or _task_started(task)): waiter = task._fut_waiter # type: ignore[attr-defined] if not isinstance(waiter, asyncio.Future) or not waiter.done(): - self._cancel_calls += 1 + origin._cancel_calls += 1 if sys.version_info >= (3, 9): task.cancel(f"Cancelled by cancel scope {id(origin):x}") else: diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index d54115f4..da84f656 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -458,6 +458,19 @@ async def test_cancel_before_entering_scope() -> None: pytest.fail("execution should not reach this point") +@pytest.mark.xfail( + sys.version_info < (3, 11), reason="Requires asyncio.Task.cancelling()" +) +@pytest.mark.parametrize("anyio_backend", ["asyncio"]) +async def test_cancel_counter_nested_scopes() -> None: + with CancelScope() as root_scope: + with CancelScope(): + root_scope.cancel() + await sleep(0.5) + + assert not cast(asyncio.Task, asyncio.current_task()).cancelling() + + async def test_exception_group_children() -> None: with pytest.raises(BaseExceptionGroup) as exc: async with create_task_group() as tg: