From 2b0bfc8df97a97c6ac88aed0ee8be5b7ec84c5fe Mon Sep 17 00:00:00 2001 From: Lura Skye Date: Mon, 14 Oct 2024 06:35:30 +0100 Subject: [PATCH 1/2] Switch backend detection to use entrypoints. This allows for external libraries to provide their own custom async backends, without needing to hack anyio internals. --- docs/versionhistory.rst | 5 +++++ pyproject.toml | 4 ++++ src/anyio/_core/_eventloop.py | 39 ++++++++++++++++++++++++++++------- tests/test_eventloop.py | 9 +++++++- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 9c228676..9e922135 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -3,6 +3,11 @@ Version history This library adheres to `Semantic Versioning 2.0 `_. +**UNRELEASED** + +- Switched backend loading to use + `import entrypoints `_. + **4.6.2** - Fixed regression caused by (`#807 `_) diff --git a/pyproject.toml b/pyproject.toml index f9ffdc77..5cf00643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,10 @@ doc = [ [project.entry-points] pytest11 = {anyio = "anyio.pytest_plugin"} +[project.entry-points."anyio.backends"] +trio = "anyio._backends._trio:backend_class" +asyncio = "anyio._backends._asyncio:backend_class" + [tool.setuptools_scm] version_scheme = "post-release" local_scheme = "dirty-tag" diff --git a/src/anyio/_core/_eventloop.py b/src/anyio/_core/_eventloop.py index 6dcb4589..5ef57d33 100644 --- a/src/anyio/_core/_eventloop.py +++ b/src/anyio/_core/_eventloop.py @@ -5,7 +5,7 @@ import threading from collections.abc import Awaitable, Callable, Generator from contextlib import contextmanager -from importlib import import_module +from importlib.metadata import EntryPoint, entry_points from typing import TYPE_CHECKING, Any, TypeVar import sniffio @@ -18,6 +18,30 @@ if TYPE_CHECKING: from ..abc import AsyncBackend + +def find_backends() -> dict[str, EntryPoint]: + """ + Loads the available backends from setuptools entrypoints. + """ + + backends: dict[str, EntryPoint] + + # for some reason, in 3.12 and above, EntryPoints.__getitem__ was changed to return a + # *single* entrypoint, rather than a list. + + if sys.version_info < (3, 12): + eps = entry_points() + found_backends = eps["anyio.backends"] + backends = {ep.name: ep for ep in found_backends} + else: + eps = entry_points(group="anyio.backends") + backends = {} + for ep in eps: + backends[ep.name] = ep + + return backends + + # This must be updated when new backends are introduced BACKENDS = "asyncio", "trio" @@ -25,6 +49,7 @@ PosArgsT = TypeVarTuple("PosArgsT") threadlocals = threading.local() +available_backends: dict[str, EntryPoint] = find_backends() loaded_backends: dict[str, type[AsyncBackend]] = {} @@ -60,7 +85,7 @@ def run( try: async_backend = get_async_backend(backend) - except ImportError as exc: + except (ImportError, KeyError) as exc: raise LookupError(f"No such backend: {backend}") from exc token = None @@ -124,8 +149,8 @@ def current_time() -> float: def get_all_backends() -> tuple[str, ...]: - """Return a tuple of the names of all built-in backends.""" - return BACKENDS + """Return a tuple of the names of all available backends.""" + return tuple(available_backends.keys()) def get_cancelled_exc_class() -> type[BaseException]: @@ -161,6 +186,6 @@ def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: try: return loaded_backends[asynclib_name] except KeyError: - module = import_module(f"anyio._backends._{asynclib_name}") - loaded_backends[asynclib_name] = module.backend_class - return module.backend_class + loaded_backend: type[AsyncBackend] = available_backends[asynclib_name].load() + loaded_backends[asynclib_name] = loaded_backend + return loaded_backend diff --git a/tests/test_eventloop.py b/tests/test_eventloop.py index 7431acf7..e9c30916 100644 --- a/tests/test_eventloop.py +++ b/tests/test_eventloop.py @@ -9,7 +9,7 @@ from pytest import MonkeyPatch from pytest_mock.plugin import MockerFixture -from anyio import run, sleep_forever, sleep_until +from anyio import get_all_backends, run, sleep_forever, sleep_until pytestmark = pytest.mark.anyio fake_current_time = 1620581544.0 @@ -48,6 +48,13 @@ async def async_add(x: int, y: int) -> int: assert result == 3 +def test_find_builtin_backends() -> None: + backends = get_all_backends() + assert len(backends) >= 2 + assert any(x == "trio" for x in backends) + assert any(x == "asyncio" for x in backends) + + class TestAsyncioOptions: def test_debug(self) -> None: async def main() -> bool: From 9dc9eca9f3277b2296b27229e7741a0941316c57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 05:40:59 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/versionhistory.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 9e922135..6c9e50fc 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -5,7 +5,7 @@ This library adheres to `Semantic Versioning 2.0 `_. **UNRELEASED** -- Switched backend loading to use +- Switched backend loading to use `import entrypoints `_. **4.6.2**