Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added opt-in locking of first detected async backend #736

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
11 changes: 11 additions & 0 deletions docs/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,21 @@ native ``run()`` function of the backend library::

trio.run(main)

Unless you're using trio-asyncio_, you will probably want to reduce the overhead caused
by dynamic backend detection by setting the ``ANYIO_BACKEND`` environment variable to
either ``asyncio`` or ``trio``, depending on your backend of choice. This will enable
AnyIO to avoid checking which flavor of async event loop you're running when you call
one of AnyIO's functions – a check that typically involves at least one system call.

.. versionchanged:: 4.0.0
On the ``asyncio`` backend, ``anyio.run()`` now uses a back-ported version of
:class:`asyncio.Runner` on Pythons older than 3.11.

.. versionchanged:: 4.4.0
Added support for forcing a specific backend via ``ANYIO_BACKEND``

.. _trio-asyncio: https://github.com/python-trio/trio-asyncio

.. _backend options:

Backend specific options
Expand Down
3 changes: 3 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/>`_.

**UNRELEASED**

- Added an opt-in performance optimization that decreases AnyIO's overhead (compared to
native calls on the selected async backend), used by setting the ``ANYIO_BACKEND``
environment variable to ``auto``, ``asyncio`` or ``trio``
- Added support for the ``from_uri()``, ``full_match()``, ``parser`` methods/properties
in ``anyio.Path``, newly added in Python 3.13
(`#737 <https://github.com/agronholm/anyio/issues/737>`_)
Expand Down
34 changes: 30 additions & 4 deletions src/anyio/_core/_eventloop.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import math
import os
import sys
import threading
from collections.abc import Awaitable, Callable, Generator
Expand All @@ -26,19 +27,26 @@

threadlocals = threading.local()
loaded_backends: dict[str, type[AsyncBackend]] = {}
forced_backend_name = os.getenv("ANYIO_BACKEND")
forced_backend: type[AsyncBackend] | None = None


def run(
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
*args: Unpack[PosArgsT],
backend: str = "asyncio",
backend: str | None = None,
backend_options: dict[str, Any] | None = None,
) -> T_Retval:
"""
Run the given coroutine function in an asynchronous event loop.

The current thread must not be already running an event loop.

The backend will be chosen using the following priority list:
* the ``backend`` argument, if not ``None``
* the ``ANYIO_BACKEND`` environment variable
* ``asyncio``

:param func: a coroutine function
:param args: positional arguments to ``func``
:param backend: name of the asynchronous event loop implementation – currently
Expand All @@ -58,6 +66,7 @@ def run(
else:
raise RuntimeError(f"Already running {asynclib_name} in this thread")

backend = backend or os.getenv("ANYIO_BACKEND") or "asyncio"
try:
async_backend = get_async_backend(backend)
except ImportError as exc:
Expand Down Expand Up @@ -124,7 +133,16 @@ def current_time() -> float:


def get_all_backends() -> tuple[str, ...]:
"""Return a tuple of the names of all built-in backends."""
"""
Return a tuple of the names of all built-in backends.

If the ``ANYIO_BACKEND`` environment variable was set, then the returned tuple will
only contain that backend.

"""
if forced_backend_name:
return (forced_backend_name,)

return BACKENDS


Expand Down Expand Up @@ -152,15 +170,23 @@ def claim_worker_thread(


def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]:
if asynclib_name is None:
asynclib_name = sniffio.current_async_library()
global forced_backend

if forced_backend is not None:
return forced_backend

# 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
# partially initialized
asynclib_name = (
asynclib_name or forced_backend_name or sniffio.current_async_library()
)
try:
return loaded_backends[asynclib_name]
except KeyError:
module = import_module(f"anyio._backends._{asynclib_name}")
loaded_backends[asynclib_name] = module.backend_class
if asynclib_name == forced_backend_name:
forced_backend = module.backend_class

return module.backend_class
27 changes: 27 additions & 0 deletions tests/test_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester

from anyio import get_all_backends
Expand Down Expand Up @@ -418,3 +419,29 @@ async def test_anyio_mark_last_fail(x):
result.assert_outcomes(
passed=2 * len(get_all_backends()), xfailed=2 * len(get_all_backends())
)


@pytest.mark.parametrize("backend_name", get_all_backends())
def test_lock_backend(
backend_name: str, testdir: Pytester, monkeypatch: MonkeyPatch
) -> None:
testdir.makepyfile(
f"""
import os
import sys

import pytest
import anyio

pytestmark = pytest.mark.anyio

async def test_sleep(anyio_backend_name):
assert anyio.get_all_backends() == (anyio_backend_name,)
assert anyio_backend_name == {backend_name!r}
await anyio.sleep(0)
"""
)

monkeypatch.setenv("ANYIO_BACKEND", backend_name)
result = testdir.runpytest_subprocess(*pytest_args)
result.assert_outcomes(passed=1)
Loading