From f22ce74950ae831532eae8f9a61614b57af39b24 Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Thu, 20 Aug 2026 15:40:39 +0100 Subject: [PATCH 1/2] build(deps): drop pytest-cases for pytest 9.1 pytest-cases 3.10.1 builds the private _pytest.python.IdMaker positionally, so it fails to import under pytest 9.1, and its SuperClosure prunes the fixture that pytest-asyncio 1.4 injects for loop factories. Both upstream fixes are unreleased, so the test extras were pinned below those versions. The suite used pytest-cases only to reference a fixture from a parametrisation. Fixture unions become plain parametrised fixtures plus pytest-lazy-fixtures lf(), which resolves references through public pytest APIs, so the pytest and pytest-asyncio bounds reopen. The uvloop loop factory hook also takes effect for the first time: pytest-asyncio 1.3 has no such hookspec, so the suite ran on the default event loop. tests/unit/test_conftest.py now asserts the running loop instead of the hook return value. Refs smarie/python-pytest-cases#385 --- pyproject.toml | 6 +- tests/conftest.py | 8 +-- tests/integration/test_channel.py | 27 +++---- tests/integration/test_component_decorator.py | 5 +- .../test_component_event_handlers.py | 10 ++- tests/integration/test_connector_pubsub.py | 40 +++++------ tests/integration/test_process_stop_cancel.py | 12 ++-- .../test_process_with_components_run.py | 18 ++--- .../test_state_backend_multiprocess.py | 3 +- tests/unit/test_channel.py | 23 +++--- tests/unit/test_conftest.py | 7 +- tests/unit/test_connector_pubsub.py | 40 +++++------ uv.lock | 70 ++++++------------- 13 files changed, 114 insertions(+), 155 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7b710568..7a5086c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,11 +80,11 @@ test = [ "moto[server]>=5.0,<6", "openai-responses>=0.11.4,<1", "optuna>=3.0,<5", - "pytest>=8.3,<9.1.0", - "pytest-asyncio>=1.0,<1.4.0", + "pytest>=8.3,<10", + "pytest-asyncio>=1.0,<2", "pytest-benchmark>=5.1.0", - "pytest-cases>=3.8,<4", "pytest-env>=1.1,<2", + "pytest-lazy-fixtures>=1.4,<2", "pytest-rerunfailures>=15.0,<17", "ray[default,tune]>=2.40.0,<3", "redis>=7.1,<9", diff --git a/tests/conftest.py b/tests/conftest.py index 66e0858e..d7352841 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import pytest import pytest_asyncio -import pytest_cases from that_depends import ContextScopes, container_context import uvloop @@ -79,14 +78,13 @@ async def DI_teardown() -> _t.AsyncGenerator[None, None]: await DI.tear_down() -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[False, True]) -def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]: +@pytest.fixture(params=[False, True], ids=["zmq_pubsub_proxy=False", "zmq_pubsub_proxy=True"]) +def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: """Returns the ZMQConnector class with the specified proxy setting. Overrides settings to control the proxy setting without mutating process env. """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}}) + testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) with override_settings(testing_settings): yield ZMQConnector diff --git a/tests/integration/test_channel.py b/tests/integration/test_channel.py index c067591e..c0f53853 100644 --- a/tests/integration/test_channel.py +++ b/tests/integration/test_channel.py @@ -6,7 +6,7 @@ from plugboard_schemas.connector import ConnectorMode, ConnectorSpec import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.connector import ( Connector, @@ -24,35 +24,30 @@ ) -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[True]) -def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]: +@pytest.fixture(params=[True], ids=["zmq_pubsub_proxy=True"]) +def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: """Returns the ZMQConnector class with the specified proxy setting. Overrides settings to control the proxy setting without mutating process env. """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}}) + testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) with override_settings(testing_settings): yield ZMQConnector -@pytest_cases.fixture -@pytest_cases.parametrize("_connector_cls", [RabbitMQConnector, zmq_connector_cls, RedisConnector]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) +def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types.""" - return _connector_cls + return request.param -@pytest_cases.fixture -@pytest_cases.parametrize( - "_connector_cls_mp", [RabbitMQConnector, zmq_connector_cls, RedisConnector] -) -def connector_cls_mp(_connector_cls_mp: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) +def connector_cls_mp(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types for use in multiprocess context.""" - return _connector_cls_mp + return request.param -@pytest_cases.parametrize("connector_cls", [RabbitMQConnector, RedisConnector]) +@pytest.mark.parametrize("connector_cls", [RabbitMQConnector, RedisConnector]) async def test_channel_broker_url_unset(connector_cls: type[Connector], job_id_ctx: str) -> None: """Test that attempting to connect a channel without the broker URL set raises an error.""" spec = ConnectorSpec(mode=ConnectorMode.PIPELINE, source="test.send", target="test.recv") diff --git a/tests/integration/test_component_decorator.py b/tests/integration/test_component_decorator.py index 87e29bef..d2f9cfc8 100644 --- a/tests/integration/test_component_decorator.py +++ b/tests/integration/test_component_decorator.py @@ -5,7 +5,6 @@ import typing as _t import pytest -import pytest_cases from plugboard.component import IOController as IO from plugboard.component.utils import component @@ -83,7 +82,7 @@ async def step(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -127,7 +126,7 @@ async def test_process_with_decorated_components( @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), diff --git a/tests/integration/test_component_event_handlers.py b/tests/integration/test_component_event_handlers.py index 1ba77fe5..a479023e 100644 --- a/tests/integration/test_component_event_handlers.py +++ b/tests/integration/test_component_event_handlers.py @@ -6,14 +6,13 @@ from pydantic import BaseModel import pytest import pytest_asyncio -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.component import Component, IOController from plugboard.connector import AsyncioConnector, Connector, ConnectorBuilder from plugboard.events import Event from plugboard.events.event import StopEvent from plugboard.schemas import ConnectorSpec -from tests.conftest import zmq_connector_cls class EventTypeAData(BaseModel): @@ -77,11 +76,10 @@ async def event_B_handler(self, evt: EventTypeB) -> None: self._event_B_count += evt.data.y -@pytest_cases.fixture(scope="function") -@pytest_cases.parametrize("_connector_cls", [AsyncioConnector, zmq_connector_cls]) -def connector_cls(_connector_cls: _t.Type[Connector]) -> _t.Type[Connector]: +@pytest.fixture(scope="function", params=[AsyncioConnector, lf("zmq_connector_cls")]) +def connector_cls(request: pytest.FixtureRequest) -> _t.Type[Connector]: """Returns a `Connector` class.""" - return _connector_cls + return request.param @pytest.fixture diff --git a/tests/integration/test_connector_pubsub.py b/tests/integration/test_connector_pubsub.py index 3ce90d58..3dc85c66 100644 --- a/tests/integration/test_connector_pubsub.py +++ b/tests/integration/test_connector_pubsub.py @@ -3,7 +3,7 @@ import typing as _t import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.connector import ( Connector, @@ -22,31 +22,29 @@ ) -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[True]) -def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]: +@pytest.fixture(params=[True], ids=["zmq_pubsub_proxy=True"]) +def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: """Returns the ZMQConnector class with the specified proxy setting. Overrides settings to control the proxy setting without mutating process env. """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}}) + testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) with override_settings(testing_settings): yield ZMQConnector -@pytest_cases.fixture -@pytest_cases.parametrize(_connector_cls=[RabbitMQConnector, zmq_connector_cls, RedisConnector]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) +def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types.""" - return _connector_cls + return request.param @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_subscribers, num_messages", [ - (connector_cls, 1, 100), - (connector_cls, 10, 100), + (1, 100), + (10, 100), ], ) async def test_pubsub_channel_single_publisher( @@ -61,11 +59,11 @@ async def test_pubsub_channel_single_publisher( @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_publishers, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_publishers, num_subscribers, num_messages", [ - (connector_cls, 10, 1, 100), - (connector_cls, 10, 10, 100), + (10, 1, 100), + (10, 10, 100), ], ) async def test_pubsub_channel_multiple_publishers( @@ -86,11 +84,11 @@ async def test_pubsub_channel_multiple_publishers( @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_topics, num_publishers, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_topics, num_publishers, num_subscribers, num_messages", [ - (connector_cls, 3, 10, 1, 100), - (connector_cls, 3, 10, 10, 100), + (3, 10, 1, 100), + (3, 10, 10, 100), ], ) async def test_pubsub_channel_multiple_topics_and_publishers( diff --git a/tests/integration/test_process_stop_cancel.py b/tests/integration/test_process_stop_cancel.py index 239dad1d..51e5e58c 100644 --- a/tests/integration/test_process_stop_cancel.py +++ b/tests/integration/test_process_stop_cancel.py @@ -7,7 +7,7 @@ import typing as _t import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.component import Component, IOController as IO from plugboard.connector import ( @@ -20,7 +20,7 @@ from plugboard.events import StopEvent from plugboard.process import LocalProcess, Process, RayProcess from plugboard.schemas import ConnectorSpec, Status -from tests.conftest import ComponentTestHelper, zmq_connector_cls +from tests.conftest import ComponentTestHelper STOP_TOLERANCE = 3 @@ -57,14 +57,14 @@ async def step(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), - (LocalProcess, zmq_connector_cls), + (LocalProcess, lf("zmq_connector_cls")), (LocalProcess, RabbitMQConnector), # (RayProcess, RayConnector), # TODO : Pubsub/StopEvent unsupported. See https://github.com/plugboard-dev/plugboard/issues/101. - (RayProcess, zmq_connector_cls), + (RayProcess, lf("zmq_connector_cls")), (RayProcess, RabbitMQConnector), ], ) @@ -137,7 +137,7 @@ async def stop_after() -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), diff --git a/tests/integration/test_process_with_components_run.py b/tests/integration/test_process_with_components_run.py index ca599d1e..bf99a997 100644 --- a/tests/integration/test_process_with_components_run.py +++ b/tests/integration/test_process_with_components_run.py @@ -10,7 +10,7 @@ from aiofile import async_open from pydantic import BaseModel import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.component import IOController as IO from plugboard.component.component import IO_READ_TIMEOUT_SECONDS @@ -26,7 +26,7 @@ from plugboard.library import FileWriter from plugboard.process import LocalProcess, Process, RayProcess from plugboard.schemas import ConnectorSpec, Status -from tests.conftest import ComponentTestHelper, zmq_connector_cls +from tests.conftest import ComponentTestHelper class A(ComponentTestHelper): @@ -84,14 +84,14 @@ def tempfile_path() -> _t.Generator[Path, None, None]: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), - (LocalProcess, zmq_connector_cls), + (LocalProcess, lf("zmq_connector_cls")), (LocalProcess, RabbitMQConnector), (RayProcess, RayConnector), - (RayProcess, zmq_connector_cls), + (RayProcess, lf("zmq_connector_cls")), (RayProcess, RabbitMQConnector), ], ) @@ -200,7 +200,7 @@ async def _status_check(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -259,7 +259,7 @@ async def step(self) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -420,7 +420,7 @@ async def handle_action(self, evt: ActionEvent) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), @@ -659,7 +659,7 @@ async def handle_message(self, event: MessageEvent) -> None: @pytest.mark.asyncio -@pytest_cases.parametrize( +@pytest.mark.parametrize( "process_cls, connector_cls", [ (LocalProcess, AsyncioConnector), diff --git a/tests/integration/test_state_backend_multiprocess.py b/tests/integration/test_state_backend_multiprocess.py index 848adf9f..02bc45a4 100644 --- a/tests/integration/test_state_backend_multiprocess.py +++ b/tests/integration/test_state_backend_multiprocess.py @@ -4,7 +4,6 @@ import pytest import pytest_asyncio -import pytest_cases from ray.util.multiprocessing import Pool import uvloop @@ -185,7 +184,7 @@ async def _inner() -> None: assert state_data_conn["times_upserted"] == 2 -@pytest_cases.parametrize("setup_backend", [setup_SqliteStateBackend, setup_PostgresStateBackend]) +@pytest.mark.parametrize("setup_backend", [setup_SqliteStateBackend, setup_PostgresStateBackend]) @pytest.mark.asyncio async def test_no_process_found_errors( setup_backend: _t.Callable[[], _t.ContextManager[StateBackend]], diff --git a/tests/unit/test_channel.py b/tests/unit/test_channel.py index f7aa0bb1..182419b5 100644 --- a/tests/unit/test_channel.py +++ b/tests/unit/test_channel.py @@ -4,7 +4,7 @@ import typing as _t import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from ray.util.multiprocessing import Pool from that_depends import ContextScopes, container_context @@ -33,23 +33,21 @@ ] -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[False]) -def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]: +@pytest.fixture(params=[False], ids=["zmq_pubsub_proxy=False"]) +def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: """Returns the ZMQConnector class with the specified proxy setting. Overrides settings to control the proxy setting without mutating process env. """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}}) + testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) with override_settings(testing_settings): yield ZMQConnector -@pytest_cases.fixture -@pytest_cases.parametrize("_connector_cls", [AsyncioConnector, RayConnector, zmq_connector_cls]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[AsyncioConnector, RayConnector, lf("zmq_connector_cls")]) +def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types.""" - return _connector_cls + return request.param @pytest.mark.asyncio @@ -84,11 +82,10 @@ async def test_channel(connector_cls: type[Connector], ray_ctx: None, job_id_ctx assert send_channel.is_closed -@pytest_cases.fixture -@pytest_cases.parametrize("_connector_cls_mp", [RayConnector, zmq_connector_cls]) -def connector_cls_mp(_connector_cls_mp: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[RayConnector, lf("zmq_connector_cls")]) +def connector_cls_mp(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types for use in multiprocess context.""" - return _connector_cls_mp + return request.param @pytest.mark.asyncio diff --git a/tests/unit/test_conftest.py b/tests/unit/test_conftest.py index d61f5bb1..34c87d69 100644 --- a/tests/unit/test_conftest.py +++ b/tests/unit/test_conftest.py @@ -1,5 +1,6 @@ """Unit tests for the shared pytest configuration.""" +import asyncio import os import pytest @@ -10,9 +11,9 @@ from tests import conftest -def test_pytest_asyncio_loop_factories_uses_uvloop() -> None: - """The shared pytest-asyncio hook should configure uvloop factories.""" - assert conftest.pytest_asyncio_loop_factories() == {"uvloop": uvloop.new_event_loop} +async def test_tests_run_on_uvloop() -> None: + """Tests should run on a uvloop event loop, supplied by the loop factory hook.""" + assert isinstance(asyncio.get_running_loop(), uvloop.Loop) @pytest.mark.parametrize("proxy_enabled", [False, True]) diff --git a/tests/unit/test_connector_pubsub.py b/tests/unit/test_connector_pubsub.py index 17858251..d1a6b5bb 100644 --- a/tests/unit/test_connector_pubsub.py +++ b/tests/unit/test_connector_pubsub.py @@ -9,7 +9,7 @@ import typing as _t import pytest -import pytest_cases +from pytest_lazy_fixtures import lf from plugboard.connector import ( AsyncioConnector, @@ -23,23 +23,21 @@ from tests.conftest import override_settings -@pytest_cases.fixture -@pytest_cases.parametrize(zmq_pubsub_proxy=[False]) -def zmq_connector_cls(zmq_pubsub_proxy: bool) -> _t.Iterator[_t.Type[ZMQConnector]]: +@pytest.fixture(params=[False], ids=["zmq_pubsub_proxy=False"]) +def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: """Returns the ZMQConnector class with the specified proxy setting. Overrides settings to control the proxy setting without mutating process env. """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": zmq_pubsub_proxy}}) + testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) with override_settings(testing_settings): yield ZMQConnector -@pytest_cases.fixture -@pytest_cases.parametrize(_connector_cls=[AsyncioConnector, zmq_connector_cls]) -def connector_cls(_connector_cls: type[Connector]) -> type[Connector]: +@pytest.fixture(params=[AsyncioConnector, lf("zmq_connector_cls")]) +def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: """Fixture for `Connector` of various types.""" - return _connector_cls + return request.param TEST_ITEMS = string.ascii_lowercase @@ -133,11 +131,11 @@ async def recv_messages_unordered(channels: list[Channel]) -> list[int]: @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_subscribers, num_messages", [ - (connector_cls, 1, 100), - (connector_cls, 10, 100), + (1, 100), + (10, 100), ], ) async def test_pubsub_channel_single_publisher( @@ -188,11 +186,11 @@ async def _test_pubsub_channel_single_publisher( @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_publishers, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_publishers, num_subscribers, num_messages", [ - (connector_cls, 10, 1, 100), - (connector_cls, 10, 10, 100), + (10, 1, 100), + (10, 10, 100), ], ) async def test_pubsub_channel_multiple_publishers( @@ -248,11 +246,11 @@ async def _test_pubsub_channel_multiple_publishers( @pytest.mark.asyncio -@pytest_cases.parametrize( - "connector_cls, num_topics, num_publishers, num_subscribers, num_messages", +@pytest.mark.parametrize( + "num_topics, num_publishers, num_subscribers, num_messages", [ - (connector_cls, 3, 10, 1, 100), - (connector_cls, 3, 10, 10, 100), + (3, 10, 1, 100), + (3, 10, 10, 100), ], ) async def test_pubsub_channel_multiple_topics_and_publishers( diff --git a/uv.lock b/uv.lock index 55ab1c0f..a389ec5a 100644 --- a/uv.lock +++ b/uv.lock @@ -1091,18 +1091,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] -[[package]] -name = "decopatch" -version = "1.4.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "makefun" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/4c/8ca1f193428cbc4d63d0f07db9b8bd96be2db8ee5deefa93e7e8a28f2812/decopatch-1.4.10.tar.gz", hash = "sha256:957f49c93f4150182c23f8fb51d13bb3213e0f17a79e09c8cca7057598b55720", size = 69538, upload-time = "2022-03-01T08:57:21.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/fa/8e4a51e1afda8d4bd73d784bfe4a60cfdeeced9bea419eff5c271180377e/decopatch-1.4.10-py2.py3-none-any.whl", hash = "sha256:e151f7f93de2b1b3fd3f3272dcc7cefd1a69f68ec1c2d8e288ecd9deb36dc5f7", size = 18015, upload-time = "2022-03-01T08:57:20.676Z" }, -] - [[package]] name = "decorator" version = "5.2.1" @@ -2601,15 +2589,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] -[[package]] -name = "makefun" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/6780ab8bc3b84a1cce3e4400aed3d64b6db7d5e227a2f75b6ded5674701a/makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947", size = 73565, upload-time = "2025-05-09T15:00:42.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/c0/4bc973defd1270b89ccaae04cef0d5fa3ea85b59b108ad2c08aeea9afb76/makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4", size = 22923, upload-time = "2025-05-09T15:00:41.042Z" }, -] - [[package]] name = "mako" version = "1.3.10" @@ -3853,8 +3832,8 @@ all = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, - { name = "pytest-cases" }, { name = "pytest-env" }, + { name = "pytest-lazy-fixtures" }, { name = "pytest-rerunfailures" }, { name = "radon" }, { name = "ray", extra = ["default", "tune"] }, @@ -3900,8 +3879,8 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, - { name = "pytest-cases" }, { name = "pytest-env" }, + { name = "pytest-lazy-fixtures" }, { name = "pytest-rerunfailures" }, { name = "ray", extra = ["default", "tune"] }, { name = "redis" }, @@ -3967,11 +3946,11 @@ all = [ { name = "openai-responses", specifier = ">=0.11.4,<1" }, { name = "optuna", specifier = ">=3.0,<5" }, { name = "pre-commit", specifier = ">=3.8,<5" }, - { name = "pytest", specifier = ">=8.3,<9.1.0" }, - { name = "pytest-asyncio", specifier = ">=1.0,<1.4.0" }, + { name = "pytest", specifier = ">=8.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.0,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, - { name = "pytest-cases", specifier = ">=3.8,<4" }, { name = "pytest-env", specifier = ">=1.1,<2" }, + { name = "pytest-lazy-fixtures", specifier = ">=1.4,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "radon", specifier = ">=6.0.1,<7" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, @@ -4014,11 +3993,11 @@ test = [ { name = "moto", extras = ["server"], specifier = ">=5.0,<6" }, { name = "openai-responses", specifier = ">=0.11.4,<1" }, { name = "optuna", specifier = ">=3.0,<5" }, - { name = "pytest", specifier = ">=8.3,<9.1.0" }, - { name = "pytest-asyncio", specifier = ">=1.0,<1.4.0" }, + { name = "pytest", specifier = ">=8.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.0,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, - { name = "pytest-cases", specifier = ">=3.8,<4" }, { name = "pytest-env", specifier = ">=1.1,<2" }, + { name = "pytest-lazy-fixtures", specifier = ">=1.4,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, { name = "redis", specifier = ">=7.1,<9" }, @@ -4504,7 +4483,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -4513,22 +4492,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -4545,31 +4524,28 @@ wheels = [ ] [[package]] -name = "pytest-cases" -version = "3.10.1" +name = "pytest-env" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decopatch" }, - { name = "makefun" }, - { name = "packaging" }, { name = "pytest" }, + { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/a2/c7abc3b606125cf3732e8e613092b0e2549365e824b5f37972125c22806b/pytest_cases-3.10.1.tar.gz", hash = "sha256:451f9e3ecd5d2d81a4362c10c441126f5b3d1ae3a7efaf59f60b1fb930df2d69", size = 1095867, upload-time = "2026-03-02T23:05:33.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/49/08ee056f9cc655e437abcf2ae399884844b623223476ae6a77244131db03/pytest_env-1.7.0.tar.gz", hash = "sha256:0c1dc1101fb8d3ab3611e8f8d657ba06c3c0c167fc85c90457e5b27f2508f43e", size = 16408, upload-time = "2026-07-21T13:09:21.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/f2/7a29fb0571562034b05c38dceabba48dcc622be5d6c5448db80779e55de7/pytest_cases-3.10.1-py2.py3-none-any.whl", hash = "sha256:0deb8a85b6132e44adbc1cfc57897c6a624ec23f48ab445a43c7d56a6b9315a4", size = 108870, upload-time = "2026-03-02T23:05:32.663Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, ] [[package]] -name = "pytest-env" -version = "1.7.0" +name = "pytest-lazy-fixtures" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, - { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/49/08ee056f9cc655e437abcf2ae399884844b623223476ae6a77244131db03/pytest_env-1.7.0.tar.gz", hash = "sha256:0c1dc1101fb8d3ab3611e8f8d657ba06c3c0c167fc85c90457e5b27f2508f43e", size = 16408, upload-time = "2026-07-21T13:09:21.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e9/19deae3ec38f0c09c59d5d4ba0b66786b49512a24a5307213956720efa69/pytest_lazy_fixtures-1.4.1.tar.gz", hash = "sha256:8684abfd4914ea01f1e2f7b24524739344421d8f3f12286fb3100fb5681e1181", size = 63077, upload-time = "2026-08-12T09:08:36.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/18/c8/c7ba9d446a61293f037eff92f99f160d3a97c0da8315c9b6ea002b66cd3a/pytest_lazy_fixtures-1.4.1-py3-none-any.whl", hash = "sha256:42213af5859ea4a3c7ec86a7f9dbb43efa9b02547460bddad041e846cdcf921d", size = 10327, upload-time = "2026-08-12T09:08:35.404Z" }, ] [[package]] From e778d47a7ef664103709f028dfc861217451aaef Mon Sep 17 00:00:00 2001 From: Toby Coleman Date: Sun, 6 Sep 2026 22:26:38 +0100 Subject: [PATCH 2/2] test: replace lazy fixtures with native parametrization --- pyproject.toml | 3 +- tests/conftest.py | 45 ++++++++++++++---- tests/integration/test_channel.py | 46 ++++++++++--------- .../test_component_event_handlers.py | 20 +++++--- tests/integration/test_connector_pubsub.py | 31 ++++++------- tests/integration/test_process_stop_cancel.py | 20 ++++---- .../test_process_with_components_run.py | 20 ++++---- tests/unit/test_channel.py | 43 +++++++++-------- tests/unit/test_conftest.py | 43 +++++++++++++++++ tests/unit/test_connector_pubsub.py | 27 ++++------- uv.lock | 20 +------- 11 files changed, 187 insertions(+), 131 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7a5086c8..bfc7fb00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,10 +81,9 @@ test = [ "openai-responses>=0.11.4,<1", "optuna>=3.0,<5", "pytest>=8.3,<10", - "pytest-asyncio>=1.0,<2", + "pytest-asyncio>=1.4,<2", "pytest-benchmark>=5.1.0", "pytest-env>=1.1,<2", - "pytest-lazy-fixtures>=1.4,<2", "pytest-rerunfailures>=15.0,<17", "ray[default,tune]>=2.40.0,<3", "redis>=7.1,<9", diff --git a/tests/conftest.py b/tests/conftest.py index d7352841..3f4c63fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import typing as _t from unittest.mock import patch +import msgspec import pytest import pytest_asyncio from that_depends import ContextScopes, container_context @@ -14,7 +15,7 @@ from plugboard.component import Component, IOController as IO from plugboard.component.io_controller import IOStreamClosedError -from plugboard.connector import ZMQConnector +from plugboard.connector import Connector from plugboard.schemas import Status from plugboard.utils.di import DI from plugboard.utils.settings import Settings @@ -30,7 +31,7 @@ def override_settings(settings: Settings) -> _t.Iterator[None]: DI.settings.reset_override_sync() -@pytest.hookimpl(optionalhook=True) +@pytest.hookimpl def pytest_asyncio_loop_factories() -> dict[str, _t.Callable[[], asyncio.AbstractEventLoop]]: """Configure pytest-asyncio to create event loops with uvloop.""" return {"uvloop": uvloop.new_event_loop} @@ -78,15 +79,39 @@ async def DI_teardown() -> _t.AsyncGenerator[None, None]: await DI.tear_down() -@pytest.fixture(params=[False, True], ids=["zmq_pubsub_proxy=False", "zmq_pubsub_proxy=True"]) -def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: - """Returns the ZMQConnector class with the specified proxy setting. +class ConnectorCase(msgspec.Struct, frozen=True): + """Connector implementation and optional ZMQ proxy setting for a test case.""" - Overrides settings to control the proxy setting without mutating process env. - """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) - with override_settings(testing_settings): - yield ZMQConnector + connector_cls: type[Connector] + zmq_pubsub_proxy: bool | None = None + + +def connector_case_id(value: object) -> str | None: + """Name connector cases while leaving other parameter IDs to pytest.""" + if not isinstance(value, ConnectorCase): + return None + name = value.connector_cls.__name__ + if value.zmq_pubsub_proxy is not None: + name += f"-zmq_pubsub_proxy={value.zmq_pubsub_proxy}" + return name + + +@contextmanager +def configured_connector(case: ConnectorCase) -> _t.Iterator[type[Connector]]: + """Apply a connector case's settings until fixture teardown, including on failure.""" + if case.zmq_pubsub_proxy is None: + yield case.connector_cls + else: + settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": case.zmq_pubsub_proxy}}) + with override_settings(settings): + yield case.connector_cls + + +@pytest.fixture +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Resolve connector cases supplied through indirect parametrization.""" + with configured_connector(request.param) as cls: + yield cls class ComponentTestHelper(Component, ABC): diff --git a/tests/integration/test_channel.py b/tests/integration/test_channel.py index c0f53853..8eb9dd78 100644 --- a/tests/integration/test_channel.py +++ b/tests/integration/test_channel.py @@ -6,7 +6,6 @@ from plugboard_schemas.connector import ConnectorMode, ConnectorSpec import pytest -from pytest_lazy_fixtures import lf from plugboard.connector import ( Connector, @@ -16,7 +15,7 @@ from plugboard.connector.redis_channel import RedisConnector from plugboard.utils import DI from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id from tests.unit.test_channel import ( # noqa: F401 TEST_ITEMS, test_channel, @@ -24,27 +23,32 @@ ) -@pytest.fixture(params=[True], ids=["zmq_pubsub_proxy=True"]) -def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: - """Returns the ZMQConnector class with the specified proxy setting. - - Overrides settings to control the proxy setting without mutating process env. - """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) - with override_settings(testing_settings): - yield ZMQConnector - - -@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) -def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return request.param +@pytest.fixture( + params=[ + ConnectorCase(RabbitMQConnector), + ConnectorCase(ZMQConnector, True), + ConnectorCase(RedisConnector), + ], + ids=connector_case_id, +) +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls -@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) -def connector_cls_mp(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types for use in multiprocess context.""" - return request.param +@pytest.fixture( + params=[ + ConnectorCase(RabbitMQConnector), + ConnectorCase(ZMQConnector, True), + ConnectorCase(RedisConnector), + ], + ids=connector_case_id, +) +def connector_cls_mp(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls @pytest.mark.parametrize("connector_cls", [RabbitMQConnector, RedisConnector]) diff --git a/tests/integration/test_component_event_handlers.py b/tests/integration/test_component_event_handlers.py index a479023e..d5b1fa94 100644 --- a/tests/integration/test_component_event_handlers.py +++ b/tests/integration/test_component_event_handlers.py @@ -6,13 +6,13 @@ from pydantic import BaseModel import pytest import pytest_asyncio -from pytest_lazy_fixtures import lf from plugboard.component import Component, IOController -from plugboard.connector import AsyncioConnector, Connector, ConnectorBuilder +from plugboard.connector import AsyncioConnector, Connector, ConnectorBuilder, ZMQConnector from plugboard.events import Event from plugboard.events.event import StopEvent from plugboard.schemas import ConnectorSpec +from tests.conftest import ConnectorCase, configured_connector, connector_case_id class EventTypeAData(BaseModel): @@ -76,10 +76,18 @@ async def event_B_handler(self, evt: EventTypeB) -> None: self._event_B_count += evt.data.y -@pytest.fixture(scope="function", params=[AsyncioConnector, lf("zmq_connector_cls")]) -def connector_cls(request: pytest.FixtureRequest) -> _t.Type[Connector]: - """Returns a `Connector` class.""" - return request.param +@pytest.fixture( + params=[ + ConnectorCase(AsyncioConnector), + ConnectorCase(ZMQConnector, False), + ConnectorCase(ZMQConnector, True), + ], + ids=connector_case_id, +) +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls @pytest.fixture diff --git a/tests/integration/test_connector_pubsub.py b/tests/integration/test_connector_pubsub.py index 3dc85c66..60a56106 100644 --- a/tests/integration/test_connector_pubsub.py +++ b/tests/integration/test_connector_pubsub.py @@ -3,7 +3,6 @@ import typing as _t import pytest -from pytest_lazy_fixtures import lf from plugboard.connector import ( Connector, @@ -11,8 +10,7 @@ ZMQConnector, ) from plugboard.connector.redis_channel import RedisConnector -from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id from tests.unit.test_connector_pubsub import ( # noqa: F401 _HASH_SEED, TEST_ITEMS, @@ -22,21 +20,18 @@ ) -@pytest.fixture(params=[True], ids=["zmq_pubsub_proxy=True"]) -def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: - """Returns the ZMQConnector class with the specified proxy setting. - - Overrides settings to control the proxy setting without mutating process env. - """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) - with override_settings(testing_settings): - yield ZMQConnector - - -@pytest.fixture(params=[RabbitMQConnector, lf("zmq_connector_cls"), RedisConnector]) -def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return request.param +@pytest.fixture( + params=[ + ConnectorCase(RabbitMQConnector), + ConnectorCase(ZMQConnector, True), + ConnectorCase(RedisConnector), + ], + ids=connector_case_id, +) +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls @pytest.mark.asyncio diff --git a/tests/integration/test_process_stop_cancel.py b/tests/integration/test_process_stop_cancel.py index 51e5e58c..1da25ff6 100644 --- a/tests/integration/test_process_stop_cancel.py +++ b/tests/integration/test_process_stop_cancel.py @@ -7,7 +7,6 @@ import typing as _t import pytest -from pytest_lazy_fixtures import lf from plugboard.component import Component, IOController as IO from plugboard.connector import ( @@ -16,11 +15,12 @@ ConnectorBuilder, RabbitMQConnector, RayConnector, + ZMQConnector, ) from plugboard.events import StopEvent from plugboard.process import LocalProcess, Process, RayProcess from plugboard.schemas import ConnectorSpec, Status -from tests.conftest import ComponentTestHelper +from tests.conftest import ComponentTestHelper, ConnectorCase, connector_case_id STOP_TOLERANCE = 3 @@ -60,13 +60,17 @@ async def step(self) -> None: @pytest.mark.parametrize( "process_cls, connector_cls", [ - (LocalProcess, AsyncioConnector), - (LocalProcess, lf("zmq_connector_cls")), - (LocalProcess, RabbitMQConnector), - # (RayProcess, RayConnector), # TODO : Pubsub/StopEvent unsupported. See https://github.com/plugboard-dev/plugboard/issues/101. - (RayProcess, lf("zmq_connector_cls")), - (RayProcess, RabbitMQConnector), + (LocalProcess, ConnectorCase(AsyncioConnector)), + (LocalProcess, ConnectorCase(ZMQConnector, False)), + (LocalProcess, ConnectorCase(ZMQConnector, True)), + (LocalProcess, ConnectorCase(RabbitMQConnector)), + # (RayProcess, ConnectorCase(RayConnector)), # TODO : Pubsub/StopEvent unsupported. See https://github.com/plugboard-dev/plugboard/issues/101. + (RayProcess, ConnectorCase(ZMQConnector, False)), + (RayProcess, ConnectorCase(ZMQConnector, True)), + (RayProcess, ConnectorCase(RabbitMQConnector)), ], + indirect=["connector_cls"], + ids=connector_case_id, ) async def test_process_stop_event( process_cls: type[Process], connector_cls: type[Connector], ray_ctx: None diff --git a/tests/integration/test_process_with_components_run.py b/tests/integration/test_process_with_components_run.py index bf99a997..2bb5e0de 100644 --- a/tests/integration/test_process_with_components_run.py +++ b/tests/integration/test_process_with_components_run.py @@ -10,7 +10,6 @@ from aiofile import async_open from pydantic import BaseModel import pytest -from pytest_lazy_fixtures import lf from plugboard.component import IOController as IO from plugboard.component.component import IO_READ_TIMEOUT_SECONDS @@ -20,13 +19,14 @@ ConnectorBuilder, RabbitMQConnector, RayConnector, + ZMQConnector, ) from plugboard.events import Event from plugboard.exceptions import ConstraintError, NotInitialisedError, ProcessStatusError from plugboard.library import FileWriter from plugboard.process import LocalProcess, Process, RayProcess from plugboard.schemas import ConnectorSpec, Status -from tests.conftest import ComponentTestHelper +from tests.conftest import ComponentTestHelper, ConnectorCase, connector_case_id class A(ComponentTestHelper): @@ -87,13 +87,17 @@ def tempfile_path() -> _t.Generator[Path, None, None]: @pytest.mark.parametrize( "process_cls, connector_cls", [ - (LocalProcess, AsyncioConnector), - (LocalProcess, lf("zmq_connector_cls")), - (LocalProcess, RabbitMQConnector), - (RayProcess, RayConnector), - (RayProcess, lf("zmq_connector_cls")), - (RayProcess, RabbitMQConnector), + (LocalProcess, ConnectorCase(AsyncioConnector)), + (LocalProcess, ConnectorCase(ZMQConnector, False)), + (LocalProcess, ConnectorCase(ZMQConnector, True)), + (LocalProcess, ConnectorCase(RabbitMQConnector)), + (RayProcess, ConnectorCase(RayConnector)), + (RayProcess, ConnectorCase(ZMQConnector, False)), + (RayProcess, ConnectorCase(ZMQConnector, True)), + (RayProcess, ConnectorCase(RabbitMQConnector)), ], + indirect=["connector_cls"], + ids=connector_case_id, ) @pytest.mark.parametrize( "iters, factor", diff --git a/tests/unit/test_channel.py b/tests/unit/test_channel.py index 182419b5..810a20be 100644 --- a/tests/unit/test_channel.py +++ b/tests/unit/test_channel.py @@ -4,7 +4,6 @@ import typing as _t import pytest -from pytest_lazy_fixtures import lf from ray.util.multiprocessing import Pool from that_depends import ContextScopes, container_context @@ -18,8 +17,7 @@ from plugboard.exceptions import ChannelClosedError from plugboard.schemas import ConnectorMode, ConnectorSpec from plugboard.utils.di import DI -from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id TEST_ITEMS = [ @@ -33,21 +31,18 @@ ] -@pytest.fixture(params=[False], ids=["zmq_pubsub_proxy=False"]) -def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: - """Returns the ZMQConnector class with the specified proxy setting. - - Overrides settings to control the proxy setting without mutating process env. - """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) - with override_settings(testing_settings): - yield ZMQConnector - - -@pytest.fixture(params=[AsyncioConnector, RayConnector, lf("zmq_connector_cls")]) -def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return request.param +@pytest.fixture( + params=[ + ConnectorCase(AsyncioConnector), + ConnectorCase(RayConnector), + ConnectorCase(ZMQConnector, False), + ], + ids=connector_case_id, +) +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls @pytest.mark.asyncio @@ -82,10 +77,14 @@ async def test_channel(connector_cls: type[Connector], ray_ctx: None, job_id_ctx assert send_channel.is_closed -@pytest.fixture(params=[RayConnector, lf("zmq_connector_cls")]) -def connector_cls_mp(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types for use in multiprocess context.""" - return request.param +@pytest.fixture( + params=[ConnectorCase(RayConnector), ConnectorCase(ZMQConnector, False)], + ids=connector_case_id, +) +def connector_cls_mp(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls @pytest.mark.asyncio diff --git a/tests/unit/test_conftest.py b/tests/unit/test_conftest.py index 34c87d69..1c1a09d0 100644 --- a/tests/unit/test_conftest.py +++ b/tests/unit/test_conftest.py @@ -6,6 +6,7 @@ import pytest import uvloop +from plugboard.connector import AsyncioConnector, ZMQConnector from plugboard.utils.di import DI from plugboard.utils.settings import Settings from tests import conftest @@ -45,3 +46,45 @@ def exercise_override() -> None: assert DI.settings.resolve_sync() is original assert dict(os.environ) == environment + + +@pytest.mark.parametrize( + "case", + [ + conftest.ConnectorCase(AsyncioConnector), + conftest.ConnectorCase(ZMQConnector, False), + conftest.ConnectorCase(ZMQConnector, True), + ], + ids=conftest.connector_case_id, +) +@pytest.mark.parametrize("raise_error", [False, True]) +def test_configured_connector_restores_settings( + monkeypatch: pytest.MonkeyPatch, case: conftest.ConnectorCase, raise_error: bool +) -> None: + """Connector cases override only explicit flags and restore settings even on failure.""" + monkeypatch.setenv("PLUGBOARD_FLAGS_ZMQ_PUBSUB_PROXY", str(not case.zmq_pubsub_proxy)) + monkeypatch.setenv("PLUGBOARD_IO_READ_TIMEOUT", "5.0") + original = DI.settings.resolve_sync() + environment = dict(os.environ) + + def exercise_case() -> None: + with conftest.configured_connector(case) as connector_cls: + assert connector_cls is case.connector_cls + settings = DI.settings.resolve_sync() + if case.zmq_pubsub_proxy is None: + assert settings is original + else: + assert settings.flags.zmq_pubsub_proxy is case.zmq_pubsub_proxy + assert settings.io_read_timeout == 5.0 + assert dict(os.environ) == environment + if raise_error: + raise RuntimeError("simulated test failure") + + if raise_error: + with pytest.raises(RuntimeError, match="simulated test failure"): + exercise_case() + else: + exercise_case() + + assert DI.settings.resolve_sync() is original + assert dict(os.environ) == environment diff --git a/tests/unit/test_connector_pubsub.py b/tests/unit/test_connector_pubsub.py index d1a6b5bb..11b20dc1 100644 --- a/tests/unit/test_connector_pubsub.py +++ b/tests/unit/test_connector_pubsub.py @@ -9,7 +9,6 @@ import typing as _t import pytest -from pytest_lazy_fixtures import lf from plugboard.connector import ( AsyncioConnector, @@ -19,25 +18,17 @@ ) from plugboard.exceptions import ChannelClosedError from plugboard.schemas import ConnectorMode, ConnectorSpec -from plugboard.utils.settings import Settings -from tests.conftest import override_settings +from tests.conftest import ConnectorCase, configured_connector, connector_case_id -@pytest.fixture(params=[False], ids=["zmq_pubsub_proxy=False"]) -def zmq_connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[_t.Type[ZMQConnector]]: - """Returns the ZMQConnector class with the specified proxy setting. - - Overrides settings to control the proxy setting without mutating process env. - """ - testing_settings = Settings.model_validate({"flags": {"zmq_pubsub_proxy": request.param}}) - with override_settings(testing_settings): - yield ZMQConnector - - -@pytest.fixture(params=[AsyncioConnector, lf("zmq_connector_cls")]) -def connector_cls(request: pytest.FixtureRequest) -> type[Connector]: - """Fixture for `Connector` of various types.""" - return request.param +@pytest.fixture( + params=[ConnectorCase(AsyncioConnector), ConnectorCase(ZMQConnector, False)], + ids=connector_case_id, +) +def connector_cls(request: pytest.FixtureRequest) -> _t.Iterator[type[Connector]]: + """Configure each connector variant for this test module.""" + with configured_connector(request.param) as cls: + yield cls TEST_ITEMS = string.ascii_lowercase diff --git a/uv.lock b/uv.lock index a389ec5a..a11b365c 100644 --- a/uv.lock +++ b/uv.lock @@ -3833,7 +3833,6 @@ all = [ { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-env" }, - { name = "pytest-lazy-fixtures" }, { name = "pytest-rerunfailures" }, { name = "radon" }, { name = "ray", extra = ["default", "tune"] }, @@ -3880,7 +3879,6 @@ test = [ { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-env" }, - { name = "pytest-lazy-fixtures" }, { name = "pytest-rerunfailures" }, { name = "ray", extra = ["default", "tune"] }, { name = "redis" }, @@ -3947,10 +3945,9 @@ all = [ { name = "optuna", specifier = ">=3.0,<5" }, { name = "pre-commit", specifier = ">=3.8,<5" }, { name = "pytest", specifier = ">=8.3,<10" }, - { name = "pytest-asyncio", specifier = ">=1.0,<2" }, + { name = "pytest-asyncio", specifier = ">=1.4,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-env", specifier = ">=1.1,<2" }, - { name = "pytest-lazy-fixtures", specifier = ">=1.4,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "radon", specifier = ">=6.0.1,<7" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, @@ -3994,10 +3991,9 @@ test = [ { name = "openai-responses", specifier = ">=0.11.4,<1" }, { name = "optuna", specifier = ">=3.0,<5" }, { name = "pytest", specifier = ">=8.3,<10" }, - { name = "pytest-asyncio", specifier = ">=1.0,<2" }, + { name = "pytest-asyncio", specifier = ">=1.4,<2" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, { name = "pytest-env", specifier = ">=1.1,<2" }, - { name = "pytest-lazy-fixtures", specifier = ">=1.4,<2" }, { name = "pytest-rerunfailures", specifier = ">=15.0,<17" }, { name = "ray", extras = ["default", "tune"], specifier = ">=2.40.0,<3" }, { name = "redis", specifier = ">=7.1,<9" }, @@ -4536,18 +4532,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, ] -[[package]] -name = "pytest-lazy-fixtures" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/e9/19deae3ec38f0c09c59d5d4ba0b66786b49512a24a5307213956720efa69/pytest_lazy_fixtures-1.4.1.tar.gz", hash = "sha256:8684abfd4914ea01f1e2f7b24524739344421d8f3f12286fb3100fb5681e1181", size = 63077, upload-time = "2026-08-12T09:08:36.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/c8/c7ba9d446a61293f037eff92f99f160d3a97c0da8315c9b6ea002b66cd3a/pytest_lazy_fixtures-1.4.1-py3-none-any.whl", hash = "sha256:42213af5859ea4a3c7ec86a7f9dbb43efa9b02547460bddad041e846cdcf921d", size = 10327, upload-time = "2026-08-12T09:08:35.404Z" }, -] - [[package]] name = "pytest-rerunfailures" version = "16.4"