From 208a37a4de8f2fd6697dc3eaf076bac7442f5628 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 14 Oct 2023 11:48:19 +0100 Subject: [PATCH 01/22] use asyncio.run(..., loop_factory) to avoid asyncio.set_event_loop_policy --- tests/test_auto_detection.py | 11 ++--- uvicorn/_compat.py | 86 ++++++++++++++++++++++++++++++++++++ uvicorn/config.py | 17 +++---- uvicorn/loops/asyncio.py | 13 ++++-- uvicorn/loops/auto.py | 18 +++++--- uvicorn/loops/uvloop.py | 9 +++- uvicorn/main.py | 4 +- uvicorn/server.py | 4 +- uvicorn/workers.py | 6 +-- 9 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 uvicorn/_compat.py diff --git a/tests/test_auto_detection.py b/tests/test_auto_detection.py index 1f79b3786..c40ab998c 100644 --- a/tests/test_auto_detection.py +++ b/tests/test_auto_detection.py @@ -1,10 +1,11 @@ import asyncio +import contextlib import importlib import pytest from uvicorn.config import Config -from uvicorn.loops.auto import auto_loop_setup +from uvicorn.loops.auto import auto_loop_factory from uvicorn.main import ServerState from uvicorn.protocols.http.auto import AutoHTTPProtocol from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol @@ -33,10 +34,10 @@ async def app(scope, receive, send): def test_loop_auto(): - auto_loop_setup() - policy = asyncio.get_event_loop_policy() - assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy) - assert type(policy).__module__.startswith(expected_loop) + loop_factory = auto_loop_factory() + with contextlib.closing(loop_factory()) as loop: + assert isinstance(loop, asyncio.AbstractEventLoop) + assert type(loop).__module__.startswith(expected_loop) @pytest.mark.anyio diff --git a/uvicorn/_compat.py b/uvicorn/_compat.py new file mode 100644 index 000000000..23ffc61f1 --- /dev/null +++ b/uvicorn/_compat.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import asyncio +import sys +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar + +_T = TypeVar("_T") + +if sys.version_info >= (3, 12): + asyncio_run = asyncio.run +elif sys.version_info >= (3, 11): + + def asyncio_run( + main: Coroutine[Any, Any, _T], + *, + debug: bool = False, + loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, + ) -> _T: + # asyncio.run from Python 3.12 + # https://docs.python.org/3/license.html#psf-license + with asyncio.Runner(debug=debug, loop_factory=loop_factory) as runner: + return runner.run(main) + +else: + # modified version of asyncio.run from Python 3.10 to add loop_factory kwarg + # https://docs.python.org/3/license.html#psf-license + def asyncio_run( + main: Coroutine[Any, Any, _T], + *, + debug: bool = False, + loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None, + ) -> _T: + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + raise RuntimeError( + "asyncio.run() cannot be called from a running event loop" + ) + + if not asyncio.iscoroutine(main): + raise ValueError(f"a coroutine was expected, got {main!r}") + + if loop_factory is None: + loop = asyncio.new_event_loop() + else: + loop = loop_factory() + try: + if loop_factory is None: + asyncio.set_event_loop(loop) + if debug is not None: + loop.set_debug(debug) + return loop.run_until_complete(main) + finally: + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.run_until_complete(loop.shutdown_default_executor()) + finally: + if loop_factory is None: + asyncio.set_event_loop(None) + loop.close() + + def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: + to_cancel = asyncio.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) diff --git a/uvicorn/config.py b/uvicorn/config.py index 9aff8c968..d672ff8db 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -53,11 +53,11 @@ "on": "uvicorn.lifespan.on:LifespanOn", "off": "uvicorn.lifespan.off:LifespanOff", } -LOOP_SETUPS: dict[LoopSetupType, str | None] = { +LOOP_FACTORIES: dict[LoopSetupType, str | None] = { "none": None, - "auto": "uvicorn.loops.auto:auto_loop_setup", - "asyncio": "uvicorn.loops.asyncio:asyncio_setup", - "uvloop": "uvicorn.loops.uvloop:uvloop_setup", + "auto": "uvicorn.loops.auto:auto_loop_factory", + "asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory", + "uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory", } INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"] @@ -471,10 +471,11 @@ def load(self) -> None: self.loaded = True - def setup_event_loop(self) -> None: - loop_setup: Callable | None = import_from_string(LOOP_SETUPS[self.loop]) - if loop_setup is not None: - loop_setup(use_subprocess=self.use_subprocess) + def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None: + loop_factory: Callable | None = import_from_string(LOOP_FACTORIES[self.loop]) + if loop_factory is None: + return None + return loop_factory(use_subprocess=self.use_subprocess) def bind_socket(self) -> socket.socket: logger_args: list[str | int] diff --git a/uvicorn/loops/asyncio.py b/uvicorn/loops/asyncio.py index 1bead4a06..a598bc075 100644 --- a/uvicorn/loops/asyncio.py +++ b/uvicorn/loops/asyncio.py @@ -1,10 +1,17 @@ +from __future__ import annotations + import asyncio import logging import sys +from collections.abc import Callable +from typing import TypeVar + +_T = TypeVar("_T") logger = logging.getLogger("uvicorn.error") -def asyncio_setup(use_subprocess: bool = False) -> None: - if sys.platform == "win32" and use_subprocess: - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage +def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: + if sys.platform == "win32" and not use_subprocess: + return asyncio.ProactorEventLoop + return asyncio.SelectorEventLoop diff --git a/uvicorn/loops/auto.py b/uvicorn/loops/auto.py index 2285457bf..333491b8b 100644 --- a/uvicorn/loops/auto.py +++ b/uvicorn/loops/auto.py @@ -1,11 +1,19 @@ -def auto_loop_setup(use_subprocess: bool = False) -> None: +from __future__ import annotations + +import asyncio +from collections.abc import Callable + + +def auto_loop_factory( + use_subprocess: bool = False, +) -> Callable[[], asyncio.AbstractEventLoop]: try: import uvloop # noqa except ImportError: # pragma: no cover - from uvicorn.loops.asyncio import asyncio_setup as loop_setup + from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory - loop_setup(use_subprocess=use_subprocess) + return loop_factory(use_subprocess=use_subprocess) else: # pragma: no cover - from uvicorn.loops.uvloop import uvloop_setup + from uvicorn.loops.uvloop import uvloop_loop_factory - uvloop_setup(use_subprocess=use_subprocess) + return uvloop_loop_factory(use_subprocess=use_subprocess) diff --git a/uvicorn/loops/uvloop.py b/uvicorn/loops/uvloop.py index 0e2fd1eb0..1d05a373c 100644 --- a/uvicorn/loops/uvloop.py +++ b/uvicorn/loops/uvloop.py @@ -1,7 +1,12 @@ +from __future__ import annotations + import asyncio +from collections.abc import Callable import uvloop -def uvloop_setup(use_subprocess: bool = False) -> None: - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) +def uvloop_loop_factory( + use_subprocess: bool = False, +) -> Callable[[], asyncio.AbstractEventLoop]: + return uvloop.new_event_loop diff --git a/uvicorn/main.py b/uvicorn/main.py index 4352efbca..bda612dfa 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -19,7 +19,7 @@ LIFESPAN, LOG_LEVELS, LOGGING_CONFIG, - LOOP_SETUPS, + LOOP_FACTORIES, SSL_PROTOCOL_VERSION, WS_PROTOCOLS, Config, @@ -36,7 +36,7 @@ HTTP_CHOICES = click.Choice(list(HTTP_PROTOCOLS.keys())) WS_CHOICES = click.Choice(list(WS_PROTOCOLS.keys())) LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys())) -LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"]) +LOOP_CHOICES = click.Choice([key for key in LOOP_FACTORIES.keys() if key != "none"]) INTERFACE_CHOICES = click.Choice(INTERFACES) STARTUP_FAILURE = 3 diff --git a/uvicorn/server.py b/uvicorn/server.py index fa7638b7d..5cc00bd60 100644 --- a/uvicorn/server.py +++ b/uvicorn/server.py @@ -16,6 +16,7 @@ import click +from uvicorn._compat import asyncio_run from uvicorn.config import Config if TYPE_CHECKING: @@ -61,8 +62,7 @@ def __init__(self, config: Config) -> None: self._captured_signals: list[int] = [] def run(self, sockets: list[socket.socket] | None = None) -> None: - self.config.setup_event_loop() - return asyncio.run(self.serve(sockets=sockets)) + return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory()) async def serve(self, sockets: list[socket.socket] | None = None) -> None: with self.capture_signals(): diff --git a/uvicorn/workers.py b/uvicorn/workers.py index 061805b6c..e815f49f2 100644 --- a/uvicorn/workers.py +++ b/uvicorn/workers.py @@ -10,6 +10,7 @@ from gunicorn.arbiter import Arbiter from gunicorn.workers.base import Worker +from uvicorn._compat import asyncio_run from uvicorn.config import Config from uvicorn.main import Server @@ -71,8 +72,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.config = Config(**config_kwargs) def init_process(self) -> None: - self.config.setup_event_loop() - super().init_process() + super(UvicornWorker, self).init_process() def init_signals(self) -> None: # Reset signals so Gunicorn doesn't swallow subprocess return codes @@ -104,7 +104,7 @@ async def _serve(self) -> None: sys.exit(Arbiter.WORKER_BOOT_ERROR) def run(self) -> None: - return asyncio.run(self._serve()) + return asyncio_run(self._serve(), loop_factory=self.config.get_loop_factory()) async def callback_notify(self) -> None: self.notify() From 8021788fc30db813434426c610fc450613920c4b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 14 Oct 2023 12:09:12 +0100 Subject: [PATCH 02/22] only shutdown the default executor on 3.9 --- uvicorn/_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uvicorn/_compat.py b/uvicorn/_compat.py index 23ffc61f1..560aa89d1 100644 --- a/uvicorn/_compat.py +++ b/uvicorn/_compat.py @@ -57,7 +57,8 @@ def asyncio_run( try: _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) - loop.run_until_complete(loop.shutdown_default_executor()) + if sys.version_info >= (3, 9): + loop.run_until_complete(loop.shutdown_default_executor()) finally: if loop_factory is None: asyncio.set_event_loop(None) From db8f9a1013e6e8090eccf8a781bb4fe317941478 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 20 Oct 2023 12:58:47 +0100 Subject: [PATCH 03/22] rename LoopSetupType to LoopFactoryType --- uvicorn/config.py | 6 +++--- uvicorn/main.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/uvicorn/config.py b/uvicorn/config.py index d672ff8db..6e0d49a8a 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -26,7 +26,7 @@ HTTPProtocolType = Literal["auto", "h11", "httptools"] WSProtocolType = Literal["auto", "none", "websockets", "wsproto"] LifespanType = Literal["auto", "on", "off"] -LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"] +LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop"] InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"] LOG_LEVELS: dict[str, int] = { @@ -53,7 +53,7 @@ "on": "uvicorn.lifespan.on:LifespanOn", "off": "uvicorn.lifespan.off:LifespanOff", } -LOOP_FACTORIES: dict[LoopSetupType, str | None] = { +LOOP_FACTORIES: dict[LoopFactoryType, str | None] = { "none": None, "auto": "uvicorn.loops.auto:auto_loop_factory", "asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory", @@ -180,7 +180,7 @@ def __init__( port: int = 8000, uds: str | None = None, fd: int | None = None, - loop: LoopSetupType = "auto", + loop: LoopFactoryType = "auto", http: type[asyncio.Protocol] | HTTPProtocolType = "auto", ws: type[asyncio.Protocol] | WSProtocolType = "auto", ws_max_size: int = 16 * 1024 * 1024, diff --git a/uvicorn/main.py b/uvicorn/main.py index bda612dfa..22b403501 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -26,7 +26,7 @@ HTTPProtocolType, InterfaceType, LifespanType, - LoopSetupType, + LoopFactoryType, WSProtocolType, ) from uvicorn.server import Server, ServerState # noqa: F401 # Used to be defined here. @@ -364,7 +364,7 @@ def main( port: int, uds: str, fd: int, - loop: LoopSetupType, + loop: LoopFactoryType, http: HTTPProtocolType, ws: WSProtocolType, ws_max_size: int, @@ -465,7 +465,7 @@ def run( port: int = 8000, uds: str | None = None, fd: int | None = None, - loop: LoopSetupType = "auto", + loop: LoopFactoryType = "auto", http: type[asyncio.Protocol] | HTTPProtocolType = "auto", ws: type[asyncio.Protocol] | WSProtocolType = "auto", ws_max_size: int = 16777216, From 62472229ea21c951da153ebb72bc32b894d36376 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 20 Oct 2023 13:19:50 +0100 Subject: [PATCH 04/22] remove redundant UvicornWorker.init_process --- uvicorn/workers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/uvicorn/workers.py b/uvicorn/workers.py index e815f49f2..25fa8533c 100644 --- a/uvicorn/workers.py +++ b/uvicorn/workers.py @@ -71,9 +71,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.config = Config(**config_kwargs) - def init_process(self) -> None: - super(UvicornWorker, self).init_process() - def init_signals(self) -> None: # Reset signals so Gunicorn doesn't swallow subprocess return codes # other signals are set up by Server.install_signal_handlers() From a8df5b9050d871c069ed8748c72b5eeeabc75e42 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 14 Apr 2024 22:34:43 +0200 Subject: [PATCH 05/22] fix linter --- uvicorn/loops/asyncio.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/uvicorn/loops/asyncio.py b/uvicorn/loops/asyncio.py index a598bc075..ad6121ee0 100644 --- a/uvicorn/loops/asyncio.py +++ b/uvicorn/loops/asyncio.py @@ -1,14 +1,8 @@ from __future__ import annotations import asyncio -import logging import sys from collections.abc import Callable -from typing import TypeVar - -_T = TypeVar("_T") - -logger = logging.getLogger("uvicorn.error") def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: From 849169f438c9e502d6d95b5d11062aa6bd318ddb Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 14 Apr 2024 22:34:43 +0200 Subject: [PATCH 06/22] fix linter --- uvicorn/_compat.py | 4 +--- uvicorn/loops/auto.py | 4 +--- uvicorn/loops/uvloop.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/uvicorn/_compat.py b/uvicorn/_compat.py index 560aa89d1..e2650507a 100644 --- a/uvicorn/_compat.py +++ b/uvicorn/_compat.py @@ -36,9 +36,7 @@ def asyncio_run( except RuntimeError: pass else: - raise RuntimeError( - "asyncio.run() cannot be called from a running event loop" - ) + raise RuntimeError("asyncio.run() cannot be called from a running event loop") if not asyncio.iscoroutine(main): raise ValueError(f"a coroutine was expected, got {main!r}") diff --git a/uvicorn/loops/auto.py b/uvicorn/loops/auto.py index 333491b8b..190839905 100644 --- a/uvicorn/loops/auto.py +++ b/uvicorn/loops/auto.py @@ -4,9 +4,7 @@ from collections.abc import Callable -def auto_loop_factory( - use_subprocess: bool = False, -) -> Callable[[], asyncio.AbstractEventLoop]: +def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: try: import uvloop # noqa except ImportError: # pragma: no cover diff --git a/uvicorn/loops/uvloop.py b/uvicorn/loops/uvloop.py index 1d05a373c..c6692c58f 100644 --- a/uvicorn/loops/uvloop.py +++ b/uvicorn/loops/uvloop.py @@ -6,7 +6,5 @@ import uvloop -def uvloop_loop_factory( - use_subprocess: bool = False, -) -> Callable[[], asyncio.AbstractEventLoop]: +def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]: return uvloop.new_event_loop From 2dafe54bda2cc8978e529f4aa40c9d15e1944b55 Mon Sep 17 00:00:00 2001 From: Nir Geller Date: Sun, 18 Aug 2024 19:58:22 +0300 Subject: [PATCH 07/22] Fix coverage --- pyproject.toml | 2 +- tests/test_auto_detection.py | 2 +- tests/test_config.py | 23 ++++++++++++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2242d424a..5400a68ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ filterwarnings = [ [tool.coverage.run] source_pkgs = ["uvicorn", "tests"] plugins = ["coverage_conditional_plugin"] -omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] +omit = ["uvicorn/workers.py", "uvicorn/__main__.py", "uvicorn/_compat.py"] [tool.coverage.report] precision = 2 diff --git a/tests/test_auto_detection.py b/tests/test_auto_detection.py index c40ab998c..ef86bf265 100644 --- a/tests/test_auto_detection.py +++ b/tests/test_auto_detection.py @@ -34,7 +34,7 @@ async def app(scope, receive, send): def test_loop_auto(): - loop_factory = auto_loop_factory() + loop_factory = auto_loop_factory(use_subprocess=True) with contextlib.closing(loop_factory()) as loop: assert isinstance(loop, asyncio.AbstractEventLoop) assert type(loop).__module__.startswith(expected_loop) diff --git a/tests/test_config.py b/tests/test_config.py index e16cc5d56..48c2b1c10 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import configparser import io import json @@ -8,6 +9,7 @@ import socket import sys import typing +from contextlib import closing from pathlib import Path from typing import Any, Literal from unittest.mock import MagicMock @@ -25,7 +27,7 @@ Scope, StartResponse, ) -from uvicorn.config import Config +from uvicorn.config import Config, LoopFactoryType from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.wsgi import WSGIMiddleware from uvicorn.protocols.http.h11_impl import H11Protocol @@ -545,3 +547,22 @@ def test_warn_when_using_reload_and_workers(caplog: pytest.LogCaptureFixture) -> Config(app=asgi_app, reload=True, workers=2) assert len(caplog.records) == 1 assert '"workers" flag is ignored when reloading is enabled.' in caplog.records[0].message + + +@pytest.mark.parametrize( + ("loop_type", "expected_loop_factory"), + [ + ("none", None), + ("asyncio", asyncio.ProactorEventLoop if sys.platform == "win32" else asyncio.SelectorEventLoop), # type: ignore + ], +) +def test_get_loop_factory(loop_type: LoopFactoryType, expected_loop_factory: Any): + config = Config(app=asgi_app, loop=loop_type) + loop_factory = config.get_loop_factory() + if loop_factory is None: + assert expected_loop_factory is loop_factory + else: + loop = loop_factory() + with closing(loop): + assert loop is not None + assert isinstance(loop, expected_loop_factory) From 100d1ad1188b2612f9dfffb04688d48912887838 Mon Sep 17 00:00:00 2001 From: Nir Geller Date: Sun, 18 Aug 2024 20:27:58 +0300 Subject: [PATCH 08/22] Allow passing a custom loop --- docs/deployment.md | 6 +++++- docs/index.md | 6 +++++- tests/custom_loop_utils.py | 12 ++++++++++++ tests/test_config.py | 32 +++++++++++++++++++++++++++++--- tests/utils.py | 9 +++++++++ uvicorn/config.py | 13 ++++++++++--- uvicorn/main.py | 12 ++++++------ 7 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 tests/custom_loop_utils.py diff --git a/docs/deployment.md b/docs/deployment.md index 7a2c7972c..241b533cc 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -57,7 +57,11 @@ Options: --workers INTEGER Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available, or 1. Not valid with --reload. - --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] + --loop TEXT Event loop implementation. Can be one of + [auto|asyncio|uvloop] or an import string to + a function of type: (use_subprocess: bool) + -> Callable[[], asyncio.AbstractEventLoop]. + [default: auto] --http [auto|h11|httptools] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] diff --git a/docs/index.md b/docs/index.md index 5d805316b..e741e75b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -127,7 +127,11 @@ Options: --workers INTEGER Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available, or 1. Not valid with --reload. - --loop [auto|asyncio|uvloop] Event loop implementation. [default: auto] + --loop TEXT Event loop implementation. Can be one of + [auto|asyncio|uvloop] or an import string to + a function of type: (use_subprocess: bool) + -> Callable[[], asyncio.AbstractEventLoop]. + [default: auto] --http [auto|h11|httptools] HTTP protocol implementation. [default: auto] --ws [auto|none|websockets|wsproto] diff --git a/tests/custom_loop_utils.py b/tests/custom_loop_utils.py new file mode 100644 index 000000000..3a2db4a78 --- /dev/null +++ b/tests/custom_loop_utils.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import asyncio +from asyncio import AbstractEventLoop + + +class CustomLoop(asyncio.SelectorEventLoop): + pass + + +def custom_loop_factory(use_subprocess: bool) -> type[AbstractEventLoop]: + return CustomLoop diff --git a/tests/test_config.py b/tests/test_config.py index 48c2b1c10..fcbce4880 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import configparser import io import json @@ -18,7 +17,8 @@ import yaml from pytest_mock import MockerFixture -from tests.utils import as_cwd +from tests.custom_loop_utils import CustomLoop +from tests.utils import as_cwd, get_asyncio_default_loop_per_os from uvicorn._types import ( ASGIApplication, ASGIReceiveCallable, @@ -553,7 +553,7 @@ def test_warn_when_using_reload_and_workers(caplog: pytest.LogCaptureFixture) -> ("loop_type", "expected_loop_factory"), [ ("none", None), - ("asyncio", asyncio.ProactorEventLoop if sys.platform == "win32" else asyncio.SelectorEventLoop), # type: ignore + ("asyncio", get_asyncio_default_loop_per_os()), ], ) def test_get_loop_factory(loop_type: LoopFactoryType, expected_loop_factory: Any): @@ -566,3 +566,29 @@ def test_get_loop_factory(loop_type: LoopFactoryType, expected_loop_factory: Any with closing(loop): assert loop is not None assert isinstance(loop, expected_loop_factory) + + +def test_custom_loop__importable_custom_loop_setup_function() -> None: + config = Config(app=asgi_app, loop="tests.custom_loop_utils:custom_loop_factory") + config.load() + loop_factory = config.get_loop_factory() + assert loop_factory, "Loop factory should be set" + event_loop = loop_factory() + with closing(event_loop): + assert event_loop is not None + assert isinstance(event_loop, CustomLoop) + + +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +def test_custom_loop__not_importable_custom_loop_setup_function(caplog: pytest.LogCaptureFixture) -> None: + config = Config(app=asgi_app, loop="tests.test_config:non_existing_setup_function") + config.load() + with pytest.raises(SystemExit): + config.get_loop_factory() + error_messages = [ + record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR" + ] + assert ( + 'Error loading custom loop setup function. Attribute "non_existing_setup_function" not found in module "tests.test_config".' # noqa: E501 + == error_messages.pop(0) + ) diff --git a/tests/utils.py b/tests/utils.py index 56362f20f..8145a2bd2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,7 @@ import asyncio import os import signal +import sys from collections.abc import AsyncIterator from contextlib import asynccontextmanager, contextmanager from pathlib import Path @@ -44,3 +45,11 @@ def as_cwd(path: Path): yield finally: os.chdir(prev_cwd) + + +def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: + """Get the default asyncio loop per OS.""" + if sys.platform == "win32": + return asyncio.ProactorEventLoop # type: ignore # pragma: nocover + else: + return asyncio.SelectorEventLoop # pragma: nocover diff --git a/uvicorn/config.py b/uvicorn/config.py index 6e0d49a8a..6d2a9489e 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -53,7 +53,7 @@ "on": "uvicorn.lifespan.on:LifespanOn", "off": "uvicorn.lifespan.off:LifespanOff", } -LOOP_FACTORIES: dict[LoopFactoryType, str | None] = { +LOOP_FACTORIES: dict[str, str | None] = { "none": None, "auto": "uvicorn.loops.auto:auto_loop_factory", "asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory", @@ -180,7 +180,7 @@ def __init__( port: int = 8000, uds: str | None = None, fd: int | None = None, - loop: LoopFactoryType = "auto", + loop: str = "auto", http: type[asyncio.Protocol] | HTTPProtocolType = "auto", ws: type[asyncio.Protocol] | WSProtocolType = "auto", ws_max_size: int = 16 * 1024 * 1024, @@ -472,7 +472,14 @@ def load(self) -> None: self.loaded = True def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None: - loop_factory: Callable | None = import_from_string(LOOP_FACTORIES[self.loop]) + if self.loop in LOOP_FACTORIES: + loop_factory: Callable | None = import_from_string(LOOP_FACTORIES[self.loop]) + else: + try: + loop_factory = import_from_string(self.loop) + except ImportFromStringError as exc: + logger.error("Error loading custom loop setup function. %s" % exc) + sys.exit(1) if loop_factory is None: return None return loop_factory(use_subprocess=self.use_subprocess) diff --git a/uvicorn/main.py b/uvicorn/main.py index 22b403501..22f9e3a90 100644 --- a/uvicorn/main.py +++ b/uvicorn/main.py @@ -26,7 +26,6 @@ HTTPProtocolType, InterfaceType, LifespanType, - LoopFactoryType, WSProtocolType, ) from uvicorn.server import Server, ServerState # noqa: F401 # Used to be defined here. @@ -36,7 +35,7 @@ HTTP_CHOICES = click.Choice(list(HTTP_PROTOCOLS.keys())) WS_CHOICES = click.Choice(list(WS_PROTOCOLS.keys())) LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys())) -LOOP_CHOICES = click.Choice([key for key in LOOP_FACTORIES.keys() if key != "none"]) +LOOP_CHOICES = [key for key in LOOP_FACTORIES.keys() if key != "none"] INTERFACE_CHOICES = click.Choice(INTERFACES) STARTUP_FAILURE = 3 @@ -117,9 +116,10 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No ) @click.option( "--loop", - type=LOOP_CHOICES, + type=str, default="auto", - help="Event loop implementation.", + help=f"Event loop implementation. Can be one of [{'|'.join(LOOP_CHOICES)}] " + f"or an import string to a function of type: (use_subprocess: bool) -> Callable[[], asyncio.AbstractEventLoop].", show_default=True, ) @click.option( @@ -364,7 +364,7 @@ def main( port: int, uds: str, fd: int, - loop: LoopFactoryType, + loop: str, http: HTTPProtocolType, ws: WSProtocolType, ws_max_size: int, @@ -465,7 +465,7 @@ def run( port: int = 8000, uds: str | None = None, fd: int | None = None, - loop: LoopFactoryType = "auto", + loop: str = "auto", http: type[asyncio.Protocol] | HTTPProtocolType = "auto", ws: type[asyncio.Protocol] | WSProtocolType = "auto", ws_max_size: int = 16777216, From fcb1cbe06dc4cb0e144682e73c49f3982e6f818c Mon Sep 17 00:00:00 2001 From: Nir Geller Date: Mon, 19 Aug 2024 00:38:32 +0300 Subject: [PATCH 09/22] Add tests for `_compat.py` --- tests/test_compat.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_compat.py diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..15af6a4eb --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import asyncio +from asyncio import AbstractEventLoop + +import pytest + +from tests.custom_loop_utils import CustomLoop, custom_loop_factory +from tests.utils import get_asyncio_default_loop_per_os +from uvicorn._compat import asyncio_run + + +async def assert_event_loop(expected_loop_class: type[AbstractEventLoop]): + assert isinstance(asyncio.get_event_loop(), expected_loop_class) + + +def test_asyncio_run__default_loop_factory() -> None: + asyncio_run(assert_event_loop(get_asyncio_default_loop_per_os()), loop_factory=None) + + +def test_asyncio_run__custom_loop_factory() -> None: + asyncio_run(assert_event_loop(CustomLoop), loop_factory=custom_loop_factory(use_subprocess=False)) + + +def test_asyncio_run__passing_a_non_awaitable_callback_should_throw_error() -> None: + with pytest.raises(ValueError): + asyncio_run( + lambda: None, # type: ignore + loop_factory=custom_loop_factory(use_subprocess=False), + ) From 30b80932e97cccbd393c84e3a9c63b47a11ff595 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 08:00:07 +0000 Subject: [PATCH 10/22] fix linting --- uvicorn/_compat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/uvicorn/_compat.py b/uvicorn/_compat.py index e2650507a..f380aca50 100644 --- a/uvicorn/_compat.py +++ b/uvicorn/_compat.py @@ -55,8 +55,7 @@ def asyncio_run( try: _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) - if sys.version_info >= (3, 9): - loop.run_until_complete(loop.shutdown_default_executor()) + loop.run_until_complete(loop.shutdown_default_executor()) finally: if loop_factory is None: asyncio.set_event_loop(None) From d9db4e4ad0c6654056afead1f16d6d39cb16cde2 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 08:02:38 +0000 Subject: [PATCH 11/22] loop factory should not take use_subprocess --- tests/custom_loop_utils.py | 4 ---- tests/test_compat.py | 6 +++--- tests/test_config.py | 2 +- uvicorn/config.py | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/custom_loop_utils.py b/tests/custom_loop_utils.py index 3a2db4a78..705208c4f 100644 --- a/tests/custom_loop_utils.py +++ b/tests/custom_loop_utils.py @@ -6,7 +6,3 @@ class CustomLoop(asyncio.SelectorEventLoop): pass - - -def custom_loop_factory(use_subprocess: bool) -> type[AbstractEventLoop]: - return CustomLoop diff --git a/tests/test_compat.py b/tests/test_compat.py index 15af6a4eb..0a962030c 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -5,7 +5,7 @@ import pytest -from tests.custom_loop_utils import CustomLoop, custom_loop_factory +from tests.custom_loop_utils import CustomLoop from tests.utils import get_asyncio_default_loop_per_os from uvicorn._compat import asyncio_run @@ -19,12 +19,12 @@ def test_asyncio_run__default_loop_factory() -> None: def test_asyncio_run__custom_loop_factory() -> None: - asyncio_run(assert_event_loop(CustomLoop), loop_factory=custom_loop_factory(use_subprocess=False)) + asyncio_run(assert_event_loop(CustomLoop), loop_factory=CustomLoop) def test_asyncio_run__passing_a_non_awaitable_callback_should_throw_error() -> None: with pytest.raises(ValueError): asyncio_run( lambda: None, # type: ignore - loop_factory=custom_loop_factory(use_subprocess=False), + loop_factory=CustomLoop, ) diff --git a/tests/test_config.py b/tests/test_config.py index fcbce4880..5ad61fc56 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -569,7 +569,7 @@ def test_get_loop_factory(loop_type: LoopFactoryType, expected_loop_factory: Any def test_custom_loop__importable_custom_loop_setup_function() -> None: - config = Config(app=asgi_app, loop="tests.custom_loop_utils:custom_loop_factory") + config = Config(app=asgi_app, loop="tests.custom_loop_utils:CustomLoop") config.load() loop_factory = config.get_loop_factory() assert loop_factory, "Loop factory should be set" diff --git a/uvicorn/config.py b/uvicorn/config.py index 8922cf597..90f48b9eb 100644 --- a/uvicorn/config.py +++ b/uvicorn/config.py @@ -477,7 +477,7 @@ def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None: loop_factory: Callable | None = import_from_string(LOOP_FACTORIES[self.loop]) else: try: - loop_factory = import_from_string(self.loop) + return import_from_string(self.loop) except ImportFromStringError as exc: logger.error("Error loading custom loop setup function. %s" % exc) sys.exit(1) From 16c120b5c8a7f99981ecb793469e5abe3c7a61de Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 08:03:15 +0000 Subject: [PATCH 12/22] remove redundant import --- tests/custom_loop_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/custom_loop_utils.py b/tests/custom_loop_utils.py index 705208c4f..ab767f660 100644 --- a/tests/custom_loop_utils.py +++ b/tests/custom_loop_utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -from asyncio import AbstractEventLoop class CustomLoop(asyncio.SelectorEventLoop): From dc2c956c3233e5f0b8e3218265882efd2aaa751a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 08:08:03 +0000 Subject: [PATCH 13/22] update docs --- docs/deployment.md | 3 ++- docs/index.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/deployment.md b/docs/deployment.md index 4d5819011..1f6d520d1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -133,7 +133,8 @@ Options: --version Display the uvicorn version and exit. --app-dir TEXT Look for APP in the specified directory, by adding this to the PYTHONPATH. Defaults to - the current working directory. + the current working directory. [default: + ""] --h11-max-incomplete-event-size INTEGER For h11, the maximum number of bytes to buffer of an incomplete event. diff --git a/docs/index.md b/docs/index.md index 20da6442b..7af4e8c02 100644 --- a/docs/index.md +++ b/docs/index.md @@ -203,7 +203,8 @@ Options: --version Display the uvicorn version and exit. --app-dir TEXT Look for APP in the specified directory, by adding this to the PYTHONPATH. Defaults to - the current working directory. + the current working directory. [default: + ""] --h11-max-incomplete-event-size INTEGER For h11, the maximum number of bytes to buffer of an incomplete event. From dc77349369f19671ed6905de79d301f2faf7ed08 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 08:25:28 +0000 Subject: [PATCH 14/22] Update tests/test_compat.py --- tests/test_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_compat.py b/tests/test_compat.py index 0a962030c..52ac03e33 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -11,7 +11,7 @@ async def assert_event_loop(expected_loop_class: type[AbstractEventLoop]): - assert isinstance(asyncio.get_event_loop(), expected_loop_class) + assert isinstance(asyncio.get_running_loop(), expected_loop_class) def test_asyncio_run__default_loop_factory() -> None: From 4d1dec7c051803d890f632fdee19c2b5382a52d4 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:16:12 +0000 Subject: [PATCH 15/22] use asyncio.EventLoop on 3.13+ --- tests/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 8145a2bd2..2f62bd909 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,7 +49,8 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" + if sys.version_info >= (3, 13): # pragma: nocover + return asyncio.EventLoop if sys.platform == "win32": return asyncio.ProactorEventLoop # type: ignore # pragma: nocover - else: - return asyncio.SelectorEventLoop # pragma: nocover + return asyncio.SelectorEventLoop # pragma: nocover From 606dbd915b3be16439f5b8e6e6b09881ed68b28c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:29:12 +0000 Subject: [PATCH 16/22] sort out coverage --- pyproject.toml | 2 ++ tests/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c2ccb87f..6c29f96f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,3 +132,5 @@ py-gte-310 = "sys_version_info >= (3, 10)" py-lt-310 = "sys_version_info < (3, 10)" py-gte-311 = "sys_version_info >= (3, 11)" py-lt-311 = "sys_version_info < (3, 11)" +py-gte-313 = "sys_version_info >= (3, 13)" +py-lt-313 = "sys_version_info < (3, 13)" diff --git a/tests/utils.py b/tests/utils.py index 2f62bd909..def53958a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,8 +49,8 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" - if sys.version_info >= (3, 13): # pragma: nocover + if sys.version_info >= (3, 13): # pragma: py-lt-313 return asyncio.EventLoop - if sys.platform == "win32": - return asyncio.ProactorEventLoop # type: ignore # pragma: nocover - return asyncio.SelectorEventLoop # pragma: nocover + if sys.platform == "win32": # pragma: py-no-win32 # pragma: py-gte-313 + return asyncio.ProactorEventLoop # type: ignore + return asyncio.SelectorEventLoop # pragma: py-gte-313 From 165bda1ca568aecbcb9fe64a4ce0131271228f56 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:42:56 +0000 Subject: [PATCH 17/22] fix coverage more --- tests/utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index def53958a..d0b1db4ad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,8 +49,10 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" - if sys.version_info >= (3, 13): # pragma: py-lt-313 - return asyncio.EventLoop - if sys.platform == "win32": # pragma: py-no-win32 # pragma: py-gte-313 - return asyncio.ProactorEventLoop # type: ignore - return asyncio.SelectorEventLoop # pragma: py-gte-313 + return ( + asyncio.EventLoop + if sys.version_info >= (3, 13) + else asyncio.ProactoEventLoop # type: ignore[attr-defined] + if sys.platform == "win32" + else asyncio.SelectorEventLoop + ) From bd58e8a27e4797b94b7f1deb37a30de57826d30c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:47:13 +0000 Subject: [PATCH 18/22] fix typo --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index d0b1db4ad..774c94f1d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -52,7 +52,7 @@ def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: return ( asyncio.EventLoop if sys.version_info >= (3, 13) - else asyncio.ProactoEventLoop # type: ignore[attr-defined] + else asyncio.ProactorEventLoop # type: ignore[attr-defined] if sys.platform == "win32" else asyncio.SelectorEventLoop ) From 5d629fa3bc5bc0dfa15a547d94bb83a5ec7de6a5 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:52:51 +0000 Subject: [PATCH 19/22] Revert "fix typo" This reverts commit bd58e8a27e4797b94b7f1deb37a30de57826d30c. --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 774c94f1d..d0b1db4ad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -52,7 +52,7 @@ def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: return ( asyncio.EventLoop if sys.version_info >= (3, 13) - else asyncio.ProactorEventLoop # type: ignore[attr-defined] + else asyncio.ProactoEventLoop # type: ignore[attr-defined] if sys.platform == "win32" else asyncio.SelectorEventLoop ) From 58419af3fa7683d812a3cafa431c753f60f9d8c3 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:52:53 +0000 Subject: [PATCH 20/22] Revert "fix coverage more" This reverts commit 165bda1ca568aecbcb9fe64a4ce0131271228f56. --- tests/utils.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index d0b1db4ad..def53958a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,10 +49,8 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" - return ( - asyncio.EventLoop - if sys.version_info >= (3, 13) - else asyncio.ProactoEventLoop # type: ignore[attr-defined] - if sys.platform == "win32" - else asyncio.SelectorEventLoop - ) + if sys.version_info >= (3, 13): # pragma: py-lt-313 + return asyncio.EventLoop + if sys.platform == "win32": # pragma: py-no-win32 # pragma: py-gte-313 + return asyncio.ProactorEventLoop # type: ignore + return asyncio.SelectorEventLoop # pragma: py-gte-313 From 2c0f7c1c6554bd42140741793079fb1e14ad36e6 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:52:55 +0000 Subject: [PATCH 21/22] Revert "sort out coverage" This reverts commit 606dbd915b3be16439f5b8e6e6b09881ed68b28c. --- pyproject.toml | 2 -- tests/utils.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c29f96f4..6c2ccb87f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,5 +132,3 @@ py-gte-310 = "sys_version_info >= (3, 10)" py-lt-310 = "sys_version_info < (3, 10)" py-gte-311 = "sys_version_info >= (3, 11)" py-lt-311 = "sys_version_info < (3, 11)" -py-gte-313 = "sys_version_info >= (3, 13)" -py-lt-313 = "sys_version_info < (3, 13)" diff --git a/tests/utils.py b/tests/utils.py index def53958a..2f62bd909 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,8 +49,8 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" - if sys.version_info >= (3, 13): # pragma: py-lt-313 + if sys.version_info >= (3, 13): # pragma: nocover return asyncio.EventLoop - if sys.platform == "win32": # pragma: py-no-win32 # pragma: py-gte-313 - return asyncio.ProactorEventLoop # type: ignore - return asyncio.SelectorEventLoop # pragma: py-gte-313 + if sys.platform == "win32": + return asyncio.ProactorEventLoop # type: ignore # pragma: nocover + return asyncio.SelectorEventLoop # pragma: nocover From 765b1e415cf431d5d7fa72b011b7a934cda16495 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 30 Dec 2024 09:52:56 +0000 Subject: [PATCH 22/22] Revert "use asyncio.EventLoop on 3.13+" This reverts commit 4d1dec7c051803d890f632fdee19c2b5382a52d4. --- tests/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 2f62bd909..8145a2bd2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,8 +49,7 @@ def as_cwd(path: Path): def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]: """Get the default asyncio loop per OS.""" - if sys.version_info >= (3, 13): # pragma: nocover - return asyncio.EventLoop if sys.platform == "win32": return asyncio.ProactorEventLoop # type: ignore # pragma: nocover - return asyncio.SelectorEventLoop # pragma: nocover + else: + return asyncio.SelectorEventLoop # pragma: nocover