From 3ccc2136a4bb99bab3297118a427318d3140b094 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Mon, 25 Aug 2025 11:39:51 +0100 Subject: [PATCH 1/7] Add uvloop --- pyproject.toml | 1 + uv.lock | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 39214324..20f77bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "structlog>=25.1.0,<26", "that-depends>=3.4.1,<4", "typer>=0.12,<1", + "uvloop>=0.21.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 533439c0..24ebf254 100644 --- a/uv.lock +++ b/uv.lock @@ -3217,6 +3217,7 @@ dependencies = [ { name = "structlog" }, { name = "that-depends" }, { name = "typer" }, + { name = "uvloop" }, ] [package.optional-dependencies] @@ -3309,6 +3310,7 @@ requires-dist = [ { name = "structlog", specifier = ">=25.1.0,<26" }, { name = "that-depends", specifier = ">=3.4.1,<4" }, { name = "typer", specifier = ">=0.12,<1" }, + { name = "uvloop", specifier = ">=0.21.0" }, { name = "websockets", marker = "extra == 'websockets'", specifier = ">=14.2,<15" }, ] provides-extras = ["aws", "azure", "gcp", "llm", "ray", "websockets"] @@ -4832,6 +4834,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/7e/f2b35278304673dcf9e8fe84b6d15531d91c59530dcf7919111f39a8d28f/uv-0.8.9-py3-none-win_arm64.whl", hash = "sha256:53332de28e9ee00effb695a15cdc70b2455d6b5f6b596d556076b5dd1fd3aa26", size = 18805689, upload-time = "2025-08-12T02:32:35.036Z" }, ] +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, +] + [[package]] name = "verspec" version = "0.1.0" From 815f4cb713bd628e6b09767b39fbd7d8daf90c18 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:05:56 +0100 Subject: [PATCH 2/7] asyncio.run -> uvloop.run --- plugboard/utils/async_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugboard/utils/async_utils.py b/plugboard/utils/async_utils.py index e267bdc8..a2d354d6 100644 --- a/plugboard/utils/async_utils.py +++ b/plugboard/utils/async_utils.py @@ -4,6 +4,8 @@ from concurrent.futures import ThreadPoolExecutor import typing as _t +import uvloop + async def gather_except(*coros: _t.Coroutine) -> list[_t.Any]: """Attempts to gather the given coroutines, raising any exceptions.""" @@ -16,7 +18,7 @@ async def gather_except(*coros: _t.Coroutine) -> list[_t.Any]: def _run_coro_in_thread(coro: _t.Coroutine, timeout: _t.Optional[float] = None) -> _t.Any: def _target() -> _t.Any: - return asyncio.run(coro) + return uvloop.run(coro) with ThreadPoolExecutor() as pool: future = pool.submit(_target) @@ -34,7 +36,7 @@ def run_coro_sync(coro: _t.Coroutine, timeout: _t.Optional[float] = None) -> _t. loop = asyncio.get_running_loop() except RuntimeError: # pragma: no cover # No loop running in current thread - return asyncio.run(coro) + return uvloop.run(coro) if loop.is_running(): # Run coroutine in new thread with dedicated event loop. From 1feddae90cc2052a516867faadfb5ffdf2e87c84 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:07:39 +0100 Subject: [PATCH 3/7] asyncio.run -> uvloop.run --- plugboard/_zmq/zmq_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugboard/_zmq/zmq_proxy.py b/plugboard/_zmq/zmq_proxy.py index 5d946094..ba25e048 100644 --- a/plugboard/_zmq/zmq_proxy.py +++ b/plugboard/_zmq/zmq_proxy.py @@ -7,6 +7,7 @@ import typing as _t from pydantic import BaseModel, Field, ValidationError +import uvloop import zmq import zmq.asyncio @@ -162,7 +163,7 @@ def _start_proxy( def _run_process(self) -> None: """Entry point for the child process.""" try: - asyncio.run(self._run()) + uvloop.run(self._run()) finally: # pragma: no cover self._close() From e3c2e1bc8bb763629e7d25cd94dc0eeb7bed9a16 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:08:03 +0100 Subject: [PATCH 4/7] Use uvloop in test --- tests/integration/test_state_backend_multiprocess.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_state_backend_multiprocess.py b/tests/integration/test_state_backend_multiprocess.py index 6d15a6dd..c641fa90 100644 --- a/tests/integration/test_state_backend_multiprocess.py +++ b/tests/integration/test_state_backend_multiprocess.py @@ -1,11 +1,11 @@ """Tests for the `StateBackend` classes that permit multiprocessing.""" -import asyncio import typing as _t import pytest import pytest_asyncio from ray.util.multiprocessing import Pool +import uvloop from plugboard.component import Component, IOController from plugboard.connector import Connector, ZMQConnector @@ -139,7 +139,7 @@ async def _inner() -> None: await comp.init() print("Component initialised.") - asyncio.run(_inner()) + uvloop.run(_inner()) # At the end of `Component.init` the component upserts itself into the state # backend, so we expect the state backend to have up to date component data afterwards @@ -161,7 +161,7 @@ def upsert_connector(conn: Connector) -> None: async def _inner() -> None: await state_backend.upsert_connector(conn) - asyncio.run(_inner()) + uvloop.run(_inner()) mp_processes = [] with Pool(2) as pool: From 6fa86aafe5e6928e6eea7d6217b9d83a677a9fd2 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:12:59 +0100 Subject: [PATCH 5/7] Use uvloop as event loop in tests --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 3d87370b..4aa9cd86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ """Configuration for the test suite.""" from abc import ABC, abstractmethod +from asyncio.events import BaseDefaultEventLoopPolicy import multiprocessing import os import typing as _t @@ -11,6 +12,7 @@ import pytest_cases import ray from that_depends import ContextScopes, container_context +import uvloop from plugboard.component import Component, IOController as IO from plugboard.component.io_controller import IOStreamClosedError @@ -19,6 +21,12 @@ from plugboard.utils.settings import Settings +@pytest.fixture(scope="session") +def event_loop_policy() -> BaseDefaultEventLoopPolicy: + """Set uvloop as the event loop policy for the test session.""" + return uvloop.EventLoopPolicy() + + @pytest.fixture(scope="session", autouse=True) def mp_set_start_method() -> None: """Set the start method for multiprocessing to 'spawn'.""" From a4406bca400f2a7e1f45c4a006e4b814269a86e8 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:23:13 +0100 Subject: [PATCH 6/7] Exclude uvloop on windows --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7db64ca..d23b9746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "structlog>=25.1.0,<26", "that-depends>=3.4.1,<4", "typer>=0.12,<1", - "uvloop>=0.21.0", + "uvloop>=0.21.0,<1 ; platform_system != 'Windows'", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 8ea1b07c..dbbd6acb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12, <4.0" resolution-markers = [ "python_full_version >= '3.14'", @@ -3217,7 +3217,7 @@ dependencies = [ { name = "structlog" }, { name = "that-depends" }, { name = "typer" }, - { name = "uvloop" }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, ] [package.optional-dependencies] @@ -3311,7 +3311,7 @@ requires-dist = [ { name = "structlog", specifier = ">=25.1.0,<26" }, { name = "that-depends", specifier = ">=3.4.1,<4" }, { name = "typer", specifier = ">=0.12,<1" }, - { name = "uvloop", specifier = ">=0.21.0" }, + { name = "uvloop", marker = "sys_platform != 'win32'", specifier = ">=0.21.0,<1" }, { name = "websockets", marker = "extra == 'websockets'", specifier = ">=14.2,<15" }, ] provides-extras = ["aws", "azure", "gcp", "llm", "ray", "websockets"] From 901d1121a0157d8393f3f10202f7770e1b29b899 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Wed, 3 Sep 2025 20:27:39 +0100 Subject: [PATCH 7/7] Use asyncio.run on windows --- plugboard/_zmq/zmq_proxy.py | 8 ++++++-- plugboard/utils/async_utils.py | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugboard/_zmq/zmq_proxy.py b/plugboard/_zmq/zmq_proxy.py index ba25e048..8134706c 100644 --- a/plugboard/_zmq/zmq_proxy.py +++ b/plugboard/_zmq/zmq_proxy.py @@ -7,11 +7,15 @@ import typing as _t from pydantic import BaseModel, Field, ValidationError -import uvloop import zmq import zmq.asyncio +try: + from uvloop import run as _asyncio_run +except ImportError: # pragma: no cover + from asyncio import run as _asyncio_run + zmq_sockopts_t: _t.TypeAlias = list[tuple[int, int | bytes | str]] ZMQ_ADDR: str = r"tcp://127.0.0.1" @@ -163,7 +167,7 @@ def _start_proxy( def _run_process(self) -> None: """Entry point for the child process.""" try: - uvloop.run(self._run()) + _asyncio_run(self._run()) finally: # pragma: no cover self._close() diff --git a/plugboard/utils/async_utils.py b/plugboard/utils/async_utils.py index a2d354d6..bd4732c6 100644 --- a/plugboard/utils/async_utils.py +++ b/plugboard/utils/async_utils.py @@ -4,7 +4,11 @@ from concurrent.futures import ThreadPoolExecutor import typing as _t -import uvloop + +try: + from uvloop import run as _asyncio_run +except ImportError: # pragma: no cover + from asyncio import run as _asyncio_run async def gather_except(*coros: _t.Coroutine) -> list[_t.Any]: @@ -18,7 +22,7 @@ async def gather_except(*coros: _t.Coroutine) -> list[_t.Any]: def _run_coro_in_thread(coro: _t.Coroutine, timeout: _t.Optional[float] = None) -> _t.Any: def _target() -> _t.Any: - return uvloop.run(coro) + return _asyncio_run(coro) with ThreadPoolExecutor() as pool: future = pool.submit(_target) @@ -36,7 +40,7 @@ def run_coro_sync(coro: _t.Coroutine, timeout: _t.Optional[float] = None) -> _t. loop = asyncio.get_running_loop() except RuntimeError: # pragma: no cover # No loop running in current thread - return uvloop.run(coro) + return _asyncio_run(coro) if loop.is_running(): # Run coroutine in new thread with dedicated event loop.