diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 9c228676..6c9e50fc 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: