Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-anyio for openSUSE:Factory checked in at 2026-03-06 18:16:52 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-anyio (Old) and /work/SRC/openSUSE:Factory/.python-anyio.new.561 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-anyio" Fri Mar 6 18:16:52 2026 rev:31 rq:1336595 version:4.12.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-anyio/python-anyio.changes 2025-12-09 12:51:15.822235595 +0100 +++ /work/SRC/openSUSE:Factory/.python-anyio.new.561/python-anyio.changes 2026-03-06 18:16:57.600089857 +0100 @@ -1,0 +2,10 @@ +Wed Mar 4 21:31:28 UTC 2026 - Dirk Müller <[email protected]> + +- update to 4.12.1: + * Changed all functions currently raising the private + NoCurrentAsyncBackend exception (since v4.12.0) to instead + raise the public NoEventLoopError exception + * Fixed anyio.functools.lru_cache not working with instance + methods + +------------------------------------------------------------------- Old: ---- anyio-4.12.0.tar.gz New: ---- anyio-4.12.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-anyio.spec ++++++ --- /var/tmp/diff_new_pack.t3IXxc/_old 2026-03-06 18:16:58.448125056 +0100 +++ /var/tmp/diff_new_pack.t3IXxc/_new 2026-03-06 18:16:58.448125056 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-anyio # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -26,7 +26,7 @@ %endif %{?sle15_python_module_pythons} Name: python-anyio%{psuffix} -Version: 4.12.0 +Version: 4.12.1 Release: 0 Summary: High level compatibility layer for asynchronous event loop implementations License: MIT ++++++ anyio-4.12.0.tar.gz -> anyio-4.12.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/.github/workflows/publish.yml new/anyio-4.12.1/.github/workflows/publish.yml --- old/anyio-4.12.0/.github/workflows/publish.yml 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/.github/workflows/publish.yml 2026-01-06 12:44:46.000000000 +0100 @@ -24,7 +24,7 @@ - name: Create packages run: python -m build - name: Archive packages - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: dist path: dist @@ -38,7 +38,7 @@ id-token: write steps: - name: Retrieve packages - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: dist path: dist diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/.github/workflows/test.yml new/anyio-4.12.1/.github/workflows/test.yml --- old/anyio-4.12.0/.github/workflows/test.yml 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/.github/workflows/test.yml 2026-01-06 12:44:46.000000000 +0100 @@ -2,7 +2,7 @@ on: push: - branches: [master] + branches: [master, "4.12.x"] pull_request: jobs: @@ -47,7 +47,7 @@ uses: actions/setup-python@v6 with: python-version: 3.x - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache/pip key: pip-pyright diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/.pre-commit-config.yaml new/anyio-4.12.1/.pre-commit-config.yaml --- old/anyio-4.12.0/.pre-commit-config.yaml 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/.pre-commit-config.yaml 2026-01-06 12:44:46.000000000 +0100 @@ -29,14 +29,14 @@ - tomli - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.14.10 hooks: - id: ruff-check args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/PKG-INFO new/anyio-4.12.1/PKG-INFO --- old/anyio-4.12.0/PKG-INFO 2025-11-29 00:36:34.603323500 +0100 +++ new/anyio-4.12.1/PKG-INFO 2026-01-06 12:44:54.663241600 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: anyio -Version: 4.12.0 +Version: 4.12.1 Summary: High-level concurrency and networking framework on top of asyncio or Trio Author-email: Alex Grönholm <[email protected]> License-Expression: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/docs/versionhistory.rst new/anyio-4.12.1/docs/versionhistory.rst --- old/anyio-4.12.0/docs/versionhistory.rst 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/docs/versionhistory.rst 2026-01-06 12:44:46.000000000 +0100 @@ -3,6 +3,14 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_. +**4.12.1** + +- Changed all functions currently raising the private ``NoCurrentAsyncBackend`` + exception (since v4.12.0) to instead raise the public ``NoEventLoopError`` exception + (`#1048 <https://github.com/agronholm/anyio/issues/1048>`_) +- Fixed ``anyio.functools.lru_cache`` not working with instance methods + (`#1042 <https://github.com/agronholm/anyio/issues/1042>`_) + **4.12.0** - Added support for asyncio's `task call graphs`_ on Python 3.14 and later when using diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_backends/_asyncio.py new/anyio-4.12.1/src/anyio/_backends/_asyncio.py --- old/anyio-4.12.0/src/anyio/_backends/_asyncio.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_backends/_asyncio.py 2026-01-06 12:44:46.000000000 +0100 @@ -1015,29 +1015,6 @@ _threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") -class BlockingPortal(abc.BlockingPortal): - def __new__(cls) -> BlockingPortal: - return object.__new__(cls) - - def __init__(self) -> None: - super().__init__() - self._loop = get_running_loop() - - def _spawn_task_from_thread( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - name: object, - future: Future[T_Retval], - ) -> None: - AsyncIOBackend.run_sync_from_thread( - partial(self._task_group.start_soon, name=name), - (self._call_func, func, args, kwargs, future), - self._loop, - ) - - # # Subprocesses # @@ -2599,10 +2576,6 @@ return f.result() @classmethod - def create_blocking_portal(cls) -> abc.BlockingPortal: - return BlockingPortal() - - @classmethod async def open_process( cls, command: StrOrBytesPath | Sequence[StrOrBytesPath], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_backends/_trio.py new/anyio-4.12.1/src/anyio/_backends/_trio.py --- old/anyio-4.12.0/src/anyio/_backends/_trio.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_backends/_trio.py 2026-01-06 12:44:46.000000000 +0100 @@ -17,10 +17,8 @@ Iterable, Sequence, ) -from concurrent.futures import Future from contextlib import AbstractContextManager from dataclasses import dataclass -from functools import partial from io import IOBase from os import PathLike from signal import Signals @@ -225,38 +223,6 @@ # -# Threads -# - - -class BlockingPortal(abc.BlockingPortal): - def __new__(cls) -> BlockingPortal: - return object.__new__(cls) - - def __init__(self) -> None: - super().__init__() - self._token = trio.lowlevel.current_trio_token() - - def _spawn_task_from_thread( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - name: object, - future: Future[T_Retval], - ) -> None: - trio.from_thread.run_sync( - partial(self._task_group.start_soon, name=name), - self._call_func, - func, - args, - kwargs, - future, - trio_token=self._token, - ) - - -# # Subprocesses # @@ -1115,10 +1081,6 @@ raise RunFinishedError from None @classmethod - def create_blocking_portal(cls) -> abc.BlockingPortal: - return BlockingPortal() - - @classmethod async def open_process( cls, command: StrOrBytesPath | Sequence[StrOrBytesPath], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_eventloop.py new/anyio-4.12.1/src/anyio/_core/_eventloop.py --- old/anyio-4.12.0/src/anyio/_core/_eventloop.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_eventloop.py 2026-01-06 12:44:46.000000000 +0100 @@ -9,6 +9,8 @@ from importlib import import_module from typing import TYPE_CHECKING, Any, TypeVar +from ._exceptions import NoEventLoopError + if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: @@ -118,6 +120,8 @@ Return the current value of the event loop's internal clock. :return: the clock value (seconds) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().current_time() @@ -150,7 +154,13 @@ def get_cancelled_exc_class() -> type[BaseException]: - """Return the current async library's cancellation exception class.""" + """ + Return the current async library's cancellation exception class. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ return get_async_backend().cancelled_exception_class() @@ -172,19 +182,14 @@ del threadlocals.current_token -class NoCurrentAsyncBackend(Exception): - def __init__(self) -> None: - super().__init__( - f"Not currently running on any asynchronous event loop" - f"Available async backends: {', '.join(get_all_backends())}" - ) - - def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: if asynclib_name is None: asynclib_name = current_async_library() if not asynclib_name: - raise NoCurrentAsyncBackend + raise NoEventLoopError( + f"Not currently running on any asynchronous event loop. " + f"Available async backends: {', '.join(get_all_backends())}" + ) # We use our own dict instead of sys.modules to get the already imported back-end # class because the appropriate modules in sys.modules could potentially be only diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_exceptions.py new/anyio-4.12.1/src/anyio/_core/_exceptions.py --- old/anyio-4.12.0/src/anyio/_core/_exceptions.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_exceptions.py 2026-01-06 12:44:46.000000000 +0100 @@ -136,8 +136,11 @@ class NoEventLoopError(RuntimeError): """ - Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if - not calling from an AnyIO worker thread, and no ``token`` was passed. + Raised by several functions that require an event loop to be running in the current + thread when there is no running event loop. + + This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` + if not calling from an AnyIO worker thread, and no ``token`` was passed. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_signals.py new/anyio-4.12.1/src/anyio/_core/_signals.py --- old/anyio-4.12.0/src/anyio/_core/_signals.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_signals.py 2026-01-06 12:44:46.000000000 +0100 @@ -16,6 +16,8 @@ :param signals: signals to receive (e.g. ``signal.SIGINT``) :return: an asynchronous context manager for an asynchronous iterator which yields signal numbers + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread .. warning:: Windows does not support signals natively so it is best to avoid relying on this in cross-platform applications. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_sockets.py new/anyio-4.12.1/src/anyio/_core/_sockets.py --- old/anyio-4.12.0/src/anyio/_core/_sockets.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_sockets.py 2026-01-06 12:44:46.000000000 +0100 @@ -664,6 +664,8 @@ :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) :param flags: flags to pass to upstream ``getnameinfo()`` :return: a tuple of (host name, service name) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread .. seealso:: :func:`socket.getnameinfo` @@ -687,6 +689,8 @@ socket to become readable :raises ~anyio.BusyResourceError: if another task is already waiting for the socket to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().wait_readable(sock.fileno()) @@ -711,6 +715,8 @@ socket to become writable :raises ~anyio.BusyResourceError: if another task is already waiting for the socket to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().wait_writable(sock.fileno()) @@ -742,6 +748,8 @@ object to become readable :raises ~anyio.BusyResourceError: if another task is already waiting for the object to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().wait_readable(obj) @@ -756,6 +764,8 @@ object to become writable :raises ~anyio.BusyResourceError: if another task is already waiting for the object to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread .. seealso:: See the documentation of :func:`wait_readable` for the definition of ``obj`` and notes on backend compatibility. @@ -792,6 +802,8 @@ in anyway. :param obj: an object with a ``.fileno()`` method or an integer handle + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ get_async_backend().notify_closing(obj) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_synchronization.py new/anyio-4.12.1/src/anyio/_core/_synchronization.py --- old/anyio-4.12.0/src/anyio/_core/_synchronization.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_synchronization.py 2026-01-06 12:44:46.000000000 +0100 @@ -8,8 +8,8 @@ from typing import TypeVar from ..lowlevel import checkpoint_if_cancelled -from ._eventloop import NoCurrentAsyncBackend, get_async_backend -from ._exceptions import BusyResourceError +from ._eventloop import get_async_backend +from ._exceptions import BusyResourceError, NoEventLoopError from ._tasks import CancelScope from ._testing import TaskInfo, get_current_task @@ -83,7 +83,7 @@ def __new__(cls) -> Event: try: return get_async_backend().create_event() - except NoCurrentAsyncBackend: + except NoEventLoopError: return EventAdapter() def set(self) -> None: @@ -151,7 +151,7 @@ def __new__(cls, *, fast_acquire: bool = False) -> Lock: try: return get_async_backend().create_lock(fast_acquire=fast_acquire) - except NoCurrentAsyncBackend: + except NoEventLoopError: return LockAdapter(fast_acquire=fast_acquire) async def __aenter__(self) -> None: @@ -378,7 +378,7 @@ return get_async_backend().create_semaphore( initial_value, max_value=max_value, fast_acquire=fast_acquire ) - except NoCurrentAsyncBackend: + except NoEventLoopError: return SemaphoreAdapter(initial_value, max_value=max_value) def __init__( @@ -513,7 +513,7 @@ def __new__(cls, total_tokens: float) -> CapacityLimiter: try: return get_async_backend().create_capacity_limiter(total_tokens) - except NoCurrentAsyncBackend: + except NoEventLoopError: return CapacityLimiterAdapter(total_tokens) async def __aenter__(self) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_tasks.py new/anyio-4.12.1/src/anyio/_core/_tasks.py --- old/anyio-4.12.0/src/anyio/_core/_tasks.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_tasks.py 2026-01-06 12:44:46.000000000 +0100 @@ -23,6 +23,8 @@ :param deadline: The time (clock value) when this scope is cancelled automatically :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 """ def __new__( @@ -110,6 +112,8 @@ :param shield: ``True`` to shield the cancel scope from external cancellation :return: a context manager that yields a cancel scope :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ current_time = get_async_backend().current_time @@ -131,6 +135,8 @@ ``None`` to disable the timeout :param shield: ``True`` to shield the cancel scope from external cancellation :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ deadline = ( @@ -148,6 +154,8 @@ there is no deadline in effect, or ``float('-inf')`` if the current scope has been cancelled) :rtype: float + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().current_effective_deadline() @@ -158,6 +166,8 @@ Create a task group. :return: a task group + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().create_task_group() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/_core/_testing.py new/anyio-4.12.1/src/anyio/_core/_testing.py --- old/anyio-4.12.0/src/anyio/_core/_testing.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/_core/_testing.py 2026-01-06 12:44:46.000000000 +0100 @@ -58,6 +58,8 @@ Return the current task. :return: a representation of the current task + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().get_current_task() @@ -68,6 +70,8 @@ Return a list of running tasks in the current event loop. :return: a list of task info objects + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/abc/_eventloop.py new/anyio-4.12.1/src/anyio/abc/_eventloop.py --- old/anyio-4.12.0/src/anyio/abc/_eventloop.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/abc/_eventloop.py 2026-01-06 12:44:46.000000000 +0100 @@ -33,7 +33,6 @@ from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore from .._core._tasks import CancelScope from .._core._testing import TaskInfo - from ..from_thread import BlockingPortal from ._sockets import ( ConnectedUDPSocket, ConnectedUNIXDatagramSocket, @@ -232,11 +231,6 @@ pass @classmethod - @abstractmethod - def create_blocking_portal(cls) -> BlockingPortal: - pass - - @classmethod @abstractmethod async def open_process( cls, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/from_thread.py new/anyio-4.12.1/src/anyio/from_thread.py --- old/anyio-4.12.0/src/anyio/from_thread.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/from_thread.py 2026-01-06 12:44:46.000000000 +0100 @@ -18,6 +18,7 @@ contextmanager, ) from dataclasses import dataclass, field +from functools import partial from inspect import isawaitable from threading import Lock, Thread, current_thread, get_ident from types import TracebackType @@ -30,7 +31,6 @@ ) from ._core._eventloop import ( - get_async_backend, get_cancelled_exc_class, threadlocals, ) @@ -39,7 +39,7 @@ from ._core._synchronization import Event from ._core._tasks import CancelScope, create_task_group from .abc._tasks import TaskStatus -from .lowlevel import EventLoopToken +from .lowlevel import EventLoopToken, current_token if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack @@ -183,16 +183,18 @@ class BlockingPortal: - """An object that lets external threads run code in an asynchronous event loop.""" + """ + An object that lets external threads run code in an asynchronous event loop. - def __new__(cls) -> BlockingPortal: - return get_async_backend().create_blocking_portal() + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ def __init__(self) -> None: + self._token = current_token() self._event_loop_thread_id: int | None = get_ident() self._stop_event = Event() self._task_group = create_task_group() - self._cancelled_exc_class = get_cancelled_exc_class() async def __aenter__(self) -> BlockingPortal: await self._task_group.__aenter__() @@ -257,7 +259,7 @@ retval = await retval_or_awaitable else: retval = retval_or_awaitable - except self._cancelled_exc_class: + except get_cancelled_exc_class(): future.cancel() future.set_running_or_notify_cancel() except BaseException as exc: @@ -284,8 +286,6 @@ """ Spawn a new task using the given callable. - Implementers must ensure that the future is resolved when the task finishes. - :param func: a callable :param args: positional arguments to be passed to the callable :param kwargs: keyword arguments to be passed to the callable @@ -294,7 +294,15 @@ or the exception raised during its execution """ - raise NotImplementedError + run_sync( + partial(self._task_group.start_soon, name=name), + self._call_func, + func, + args, + kwargs, + future, + token=self._token, + ) @overload def call( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/functools.py new/anyio-4.12.1/src/anyio/functools.py --- old/anyio-4.12.0/src/anyio/functools.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/functools.py 2026-01-06 12:44:46.000000000 +0100 @@ -73,11 +73,32 @@ always_checkpoint: bool +class _LRUMethodWrapper(Generic[T]): + def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object): + self.__wrapper = wrapper + self.__instance = instance + + def cache_info(self) -> AsyncCacheInfo: + return self.__wrapper.cache_info() + + def cache_parameters(self) -> AsyncCacheParameters: + return self.__wrapper.cache_parameters() + + def cache_clear(self) -> None: + self.__wrapper.cache_clear() + + async def __call__(self, *args: Any, **kwargs: Any) -> T: + if self.__instance is None: + return await self.__wrapper(*args, **kwargs) + + return await self.__wrapper(self.__instance, *args, **kwargs) + + @final class AsyncLRUCacheWrapper(Generic[P, T]): def __init__( self, - func: Callable[..., Awaitable[T]], + func: Callable[P, Awaitable[T]], maxsize: int | None, typed: bool, always_checkpoint: bool, @@ -174,6 +195,13 @@ return value + def __get__( + self, instance: object, owner: type | None = None + ) -> _LRUMethodWrapper[T]: + wrapper = _LRUMethodWrapper(self, instance) + update_wrapper(wrapper, self.__wrapped__) + return wrapper + class _LRUCacheWrapper(Generic[T]): def __init__(self, maxsize: int | None, typed: bool, always_checkpoint: bool): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/lowlevel.py new/anyio-4.12.1/src/anyio/lowlevel.py --- old/anyio-4.12.0/src/anyio/lowlevel.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/lowlevel.py 2026-01-06 12:44:46.000000000 +0100 @@ -32,7 +32,6 @@ await checkpoint_if_cancelled() await cancel_shielded_checkpoint() - .. versionadded:: 3.0 """ @@ -60,7 +59,6 @@ with CancelScope(shield=True): await checkpoint() - .. versionadded:: 3.0 """ @@ -85,6 +83,9 @@ Return a token object that can be used to call code in the current event loop from another thread. + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + .. versionadded:: 4.11.0 """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/to_process.py new/anyio-4.12.1/src/anyio/to_process.py --- old/anyio-4.12.0/src/anyio/to_process.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/to_process.py 2026-01-06 12:44:46.000000000 +0100 @@ -60,6 +60,8 @@ running :param limiter: capacity limiter to use to limit the total amount of processes running (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread :return: an awaitable that yields the return value of the function. """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio/to_thread.py new/anyio-4.12.1/src/anyio/to_thread.py --- old/anyio-4.12.0/src/anyio/to_thread.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/src/anyio/to_thread.py 2026-01-06 12:44:46.000000000 +0100 @@ -46,6 +46,8 @@ ``abandon_on_cancel`` if both parameters are passed :param limiter: capacity limiter to use to limit the total amount of threads running (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread :return: an awaitable that yields the return value of the function. """ @@ -69,6 +71,8 @@ concurrent threads. :return: a capacity limiter object + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread """ return get_async_backend().current_default_thread_limiter() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/src/anyio.egg-info/PKG-INFO new/anyio-4.12.1/src/anyio.egg-info/PKG-INFO --- old/anyio-4.12.0/src/anyio.egg-info/PKG-INFO 2025-11-29 00:36:34.000000000 +0100 +++ new/anyio-4.12.1/src/anyio.egg-info/PKG-INFO 2026-01-06 12:44:54.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: anyio -Version: 4.12.0 +Version: 4.12.1 Summary: High-level concurrency and networking framework on top of asyncio or Trio Author-email: Alex Grönholm <[email protected]> License-Expression: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-4.12.0/tests/test_functools.py new/anyio-4.12.1/tests/test_functools.py --- old/anyio-4.12.0/tests/test_functools.py 2025-11-29 00:36:29.000000000 +0100 +++ new/anyio-4.12.1/tests/test_functools.py 2026-01-06 12:44:46.000000000 +0100 @@ -14,7 +14,13 @@ get_cancelled_exc_class, wait_all_tasks_blocked, ) -from anyio.functools import cache, lru_cache, reduce +from anyio.functools import ( + AsyncLRUCacheWrapper, + _LRUMethodWrapper, + cache, + lru_cache, + reduce, +) from anyio.lowlevel import checkpoint @@ -266,6 +272,63 @@ scope.cancel() await func(1) + @staticmethod + async def _do_cache_asserts( + wrapper: _LRUMethodWrapper[int] | AsyncLRUCacheWrapper[..., int], + ) -> None: + assert wrapper.cache_parameters() == { + "always_checkpoint": False, + "maxsize": 128, + "typed": False, + } + statistics = wrapper.cache_info() + assert statistics.hits == 2 + assert statistics.misses == 2 + + wrapper.cache_clear() + statistics = wrapper.cache_info() + assert statistics.hits == 0 + assert statistics.misses == 0 + + async def test_cached_static_method(self) -> None: + class Foo: + @staticmethod + @lru_cache + async def static_method(x: int) -> int: + return x + + for _ in range(2): + assert await Foo.static_method(1) == 1 + assert await Foo.static_method(2) == 2 + + await self._do_cache_asserts(Foo.static_method) + + async def test_cached_class_method(self) -> None: + class Foo: + @classmethod + @lru_cache + async def cls_method(cls, x: int) -> int: + return x + + for _ in range(2): + assert await Foo.cls_method(1) == 1 + assert await Foo.cls_method(2) == 2 + + await self._do_cache_asserts(Foo.cls_method) + + async def test_cached_instance_method(self) -> None: + class Foo: + @lru_cache + async def instance_method(self, x: int) -> int: + return x + + foo = Foo() + for _ in range(2): + assert await foo.instance_method(1) == 1 + assert await foo.instance_method(2) == 2 + + await self._do_cache_asserts(Foo().instance_method) + class TestReduce: async def test_not_iterable(self) -> None:
