From 3cc0e6cc83f12e55a243aedf93c9ce706160b359 Mon Sep 17 00:00:00 2001 From: inoue22 <39761181+inoue22@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:42:17 +0900 Subject: [PATCH 1/3] Fixed weakref TypeError on the asyncio backend --- src/anyio/_backends/_asyncio.py | 10 ++++++++- src/anyio/_core/_tasks.py | 2 ++ tests/test_lowlevel.py | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index e64a58410..c7b756eab 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -416,7 +416,11 @@ def __enter__(self) -> CancelScope: "Each CancelScope may only be used for a single 'with' block" ) - self._host_task = host_task = cast(asyncio.Task, current_task()) + host_task = current_task() + if host_task is None: + raise RuntimeError("A cancel scope can only be entered from within a task") + + self._host_task = host_task self._tasks.add(host_task) try: task_state = _task_states[host_task] @@ -2424,6 +2428,10 @@ async def checkpoint_if_cancelled(cls) -> None: @classmethod async def cancel_shielded_checkpoint(cls) -> None: + if current_task() is None: + await sleep(0) + return + with CancelScope(shield=True): await sleep(0) diff --git a/src/anyio/_core/_tasks.py b/src/anyio/_core/_tasks.py index e95d1c159..0c5dd5a56 100644 --- a/src/anyio/_core/_tasks.py +++ b/src/anyio/_core/_tasks.py @@ -46,6 +46,8 @@ class CancelScope: :param shield: ``True`` to shield the cancel scope from external cancellation :raises NoEventLoopError: if no supported asynchronous event loop is running in the current thread + :raises RuntimeError: if the scope is entered while there is no current task in the + running event loop """ def __new__( diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 87dd7fdaa..786adfeac 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -1,10 +1,13 @@ from __future__ import annotations +import asyncio from typing import Any import pytest from anyio import create_task_group, run +from anyio._backends import _asyncio +from anyio._backends._asyncio import AsyncIOBackend, CancelScope from anyio.lowlevel import ( RunVar, cancel_shielded_checkpoint, @@ -86,6 +89,43 @@ async def second_func() -> None: assert second_finished +def test_cancel_scope_without_running_task( + asyncio_event_loop: asyncio.AbstractEventLoop, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Entering a cancel scope without a current task raises a clear error (asyncio). + + Regression test: previously this raised an obscure + ``TypeError: cannot create weak reference to 'NoneType' object`` because + ``current_task()`` returned ``None`` and that ``None`` was then used as a key in + a ``WeakKeyDictionary``. + """ + monkeypatch.setattr(_asyncio, "current_task", lambda: None) + + async def main() -> None: + with pytest.raises( + RuntimeError, match="can only be entered from within a task" + ): + with CancelScope(): + pass + + asyncio_event_loop.run_until_complete(main()) + + +def test_cancel_shielded_checkpoint_without_task( + asyncio_event_loop: asyncio.AbstractEventLoop, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """cancel_shielded_checkpoint() must not crash without a current task (asyncio). + + Library primitives such as ``CapacityLimiter`` reach this code path while + ``current_task()`` may return ``None``; it must degrade to a plain checkpoint + instead of raising ``TypeError``. + """ + monkeypatch.setattr(_asyncio, "current_task", lambda: None) + asyncio_event_loop.run_until_complete(AsyncIOBackend.cancel_shielded_checkpoint()) + + class TestRunVar: def test_get_set( self, From 284b801ae3d5a6cbb2b97e099ba8ba3df5647586 Mon Sep 17 00:00:00 2001 From: inoue22 <39761181+inoue22@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:15:18 +0900 Subject: [PATCH 2/3] Update docs/versionhistory.rst --- docs/versionhistory.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 2e62b485f..5c9650c87 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -57,6 +57,10 @@ This library adheres to `Semantic Versioning 2.0 `_. ``anyio.run()``; the options are now passed as keyword arguments to ``trio.run()`` again, as documented (a regression from AnyIO 3) (`#1161 `_; PR by @Zac-HD) +- Fixed ``TypeError: cannot create weak reference to 'NoneType' object`` on the asyncio + backend when a cancel scope or ``cancel_shielded_checkpoint()`` was used while + ``current_task()`` returned ``None`` + (`#1163 `_; PR by @inoue22) **4.13.0** From 14c229e7c38a5a72c9555a45f552c3917765760f Mon Sep 17 00:00:00 2001 From: inoue22 <39761181+inoue22@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:34:36 +0900 Subject: [PATCH 3/3] Update tests/test_lowlevel.py --- tests/test_lowlevel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py index 786adfeac..0ca987a70 100644 --- a/tests/test_lowlevel.py +++ b/tests/test_lowlevel.py @@ -104,7 +104,7 @@ def test_cancel_scope_without_running_task( async def main() -> None: with pytest.raises( - RuntimeError, match="can only be entered from within a task" + RuntimeError, match="A cancel scope can only be entered from within a task" ): with CancelScope(): pass