From 35a14c802aae221530afecf77b5f29458a319877 Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Thu, 21 May 2026 14:57:01 +0530 Subject: [PATCH 1/2] Fix event loop policy parametrization scope --- changelog.d/796.fixed.rst | 1 + pytest_asyncio/plugin.py | 37 ++++++++++++++-- tests/markers/test_function_scope.py | 63 ++++++++++++++++++++++++++++ tests/test_set_event_loop.py | 2 - 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 changelog.d/796.fixed.rst diff --git a/changelog.d/796.fixed.rst b/changelog.d/796.fixed.rst new file mode 100644 index 00000000..16b4d17c --- /dev/null +++ b/changelog.d/796.fixed.rst @@ -0,0 +1 @@ +Prevent parametrized ``event_loop_policy`` fixtures from parametrizing synchronous tests. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2fe8db12..046889d8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -729,14 +729,23 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: metafunc.definition ) if specialized_item_class is None: + if _uses_asyncio_fixtures(metafunc): + _add_fixture_to_metafunc(metafunc, "event_loop_policy") return asyncio_marker = _resolve_asyncio_marker(metafunc.definition) if asyncio_marker is None: + if _uses_asyncio_fixtures(metafunc): + _add_fixture_to_metafunc(metafunc, "event_loop_policy") return marker_loop_scope, marker_selected_factory_names = _parse_asyncio_marker( asyncio_marker ) + default_loop_scope = _get_default_test_loop_scope(metafunc.config) + loop_scope = marker_loop_scope or default_loop_scope + runner_fixture_id = f"_{loop_scope}_scoped_runner" + _add_fixture_to_metafunc(metafunc, runner_fixture_id) + _add_fixture_to_metafunc(metafunc, "event_loop_policy") hook_factories = _collect_hook_loop_factories(metafunc.config, metafunc.definition) if hook_factories is None: @@ -774,8 +783,6 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: for name in marker_selected_factory_names ] metafunc.fixturenames.append(_asyncio_loop_factory.__name__) - default_loop_scope = _get_default_test_loop_scope(metafunc.config) - loop_scope = marker_loop_scope or default_loop_scope # pytest.HIDDEN_PARAM was added in pytest 8.4 hide_id = len(factory_ids) == 1 and hasattr(pytest, "HIDDEN_PARAM") metafunc.parametrize( @@ -787,6 +794,30 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: ) +def _add_fixture_to_metafunc(metafunc: pytest.Metafunc, fixture_name: str) -> None: + if fixture_name not in metafunc.fixturenames: + metafunc.fixturenames.append(fixture_name) + + if fixture_name not in metafunc._arg2fixturedefs: + fixturemanager = metafunc.definition.session._fixturemanager + fixturedefs = fixturemanager.getfixturedefs(fixture_name, metafunc.definition) + if fixturedefs is not None: + metafunc._arg2fixturedefs[fixture_name] = fixturedefs + + +def _uses_asyncio_fixtures(metafunc: pytest.Metafunc) -> bool: + asyncio_mode = _get_asyncio_mode(metafunc.config) + for fixturedefs in metafunc._arg2fixturedefs.values(): + fixturedef = fixturedefs[-1] + fixture_func = fixturedef.func + if _is_asyncio_fixture_function(fixture_func): + return True + if asyncio_mode == Mode.AUTO and _is_coroutine_or_asyncgen(fixture_func): + return True + + return False + + @contextlib.contextmanager def _temporary_event_loop(loop: AbstractEventLoop) -> Iterator[None]: try: @@ -1073,7 +1104,7 @@ def _asyncio_loop_factory(request: FixtureRequest) -> LoopFactory | None: return getattr(request, "param", None) -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") def event_loop_policy() -> AbstractEventLoopPolicy: """Return an instance of the policy used to create asyncio event loops.""" return _get_event_loop_policy() diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 180d5c71..5081b423 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -154,6 +154,69 @@ async def test_parametrized_loop(): result.assert_outcomes(passed=2) +def test_parametrized_loop_policy_does_not_parametrize_sync_tests( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import asyncio + + import pytest + + @pytest.fixture( + scope="session", + params=[ + asyncio.get_event_loop_policy(), + asyncio.get_event_loop_policy(), + ], + ids=["policy_a", "policy_b"], + ) + def event_loop_policy(request): + return request.param + + @pytest.mark.asyncio + async def test_async(): + pass + + def test_sync(): + pass + """)) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=3) + + +def test_parametrized_loop_policy_parametrizes_sync_tests_with_async_fixtures( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import asyncio + + import pytest + import pytest_asyncio + + @pytest.fixture( + scope="session", + params=[ + asyncio.get_event_loop_policy(), + asyncio.get_event_loop_policy(), + ], + ids=["policy_a", "policy_b"], + ) + def event_loop_policy(request): + return request.param + + @pytest_asyncio.fixture + async def async_fixture(): + return True + + def test_sync_with_async_fixture(async_fixture): + assert async_fixture + """)) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + def test_event_loop_policy_fixture_override_emits_deprecation_warning( pytester: Pytester, ): diff --git a/tests/test_set_event_loop.py b/tests/test_set_event_loop.py index 52dfc7af..959efcd3 100644 --- a/tests/test_set_event_loop.py +++ b/tests/test_set_event_loop.py @@ -112,8 +112,6 @@ def test_asyncio_run_after_async_fixture_does_not_leak_loop( import pytest import pytest_asyncio - pytest_plugins = "pytest_asyncio" - @pytest_asyncio.fixture async def async_fixture(): yield From 8305b3a5479ce510a51370b225e01fee611e35bf Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Sat, 23 May 2026 14:07:33 +0530 Subject: [PATCH 2/2] Preserve event_loop_policy fixture dependencies --- pytest_asyncio/plugin.py | 18 +++++++++-------- tests/markers/test_function_scope.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 046889d8..4201fec5 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -795,14 +795,16 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: def _add_fixture_to_metafunc(metafunc: pytest.Metafunc, fixture_name: str) -> None: - if fixture_name not in metafunc.fixturenames: - metafunc.fixturenames.append(fixture_name) - - if fixture_name not in metafunc._arg2fixturedefs: - fixturemanager = metafunc.definition.session._fixturemanager - fixturedefs = fixturemanager.getfixturedefs(fixture_name, metafunc.definition) - if fixturedefs is not None: - metafunc._arg2fixturedefs[fixture_name] = fixturedefs + fixturemanager = metafunc.definition.session._fixturemanager + fixturenames_closure, arg2fixturedefs = fixturemanager.getfixtureclosure( + metafunc.definition, + (fixture_name,), + ignore_args=set(), + ) + metafunc._arg2fixturedefs.update(arg2fixturedefs) + for name in fixturenames_closure: + if name not in metafunc.fixturenames: + metafunc.fixturenames.append(name) def _uses_asyncio_fixtures(metafunc: pytest.Metafunc) -> bool: diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index 5081b423..a60faf00 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -217,6 +217,35 @@ def test_sync_with_async_fixture(async_fixture): result.assert_outcomes(passed=2) +def test_parametrized_loop_policy_can_depend_on_parametrized_fixture( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import asyncio + + import pytest + + @pytest.fixture(params=["policy_a", "policy_b"]) + def policy(request): + return request.param + + @pytest.fixture + def event_loop_policy(policy): + assert policy in {"policy_a", "policy_b"} + return asyncio.get_event_loop_policy() + + @pytest.mark.asyncio + async def test_marked_async(): + pass + + async def test_auto_async(): + pass + """)) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=4) + + def test_event_loop_policy_fixture_override_emits_deprecation_warning( pytester: Pytester, ):