From 8aca36b7811ad15eb62ac7c88fd22220093d7a52 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Fri, 28 Aug 2026 09:34:59 +0530 Subject: [PATCH] Apply loop factories when asyncio is marked via parametrize. pytest.param(..., marks=pytest.mark.asyncio) was ignored when selecting pytest_asyncio_loop_factories. --- changelog.d/1463.fixed.rst | 1 + pytest_asyncio/plugin.py | 29 ++++++++++++++++++--- tests/test_loop_factory_parametrization.py | 30 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 changelog.d/1463.fixed.rst diff --git a/changelog.d/1463.fixed.rst b/changelog.d/1463.fixed.rst new file mode 100644 index 00000000..22dbd2d5 --- /dev/null +++ b/changelog.d/1463.fixed.rst @@ -0,0 +1 @@ +Apply configured loop factories when ``pytest.mark.asyncio`` is attached through ``pytest.param`` rather than on the test function itself. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 38b75e41..a0647164 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -685,6 +685,27 @@ def _resolve_asyncio_marker(item: Function) -> Mark | None: return None +def _asyncio_marker_from_parametrize(item: Function) -> Mark | None: + """Return an asyncio mark attached via pytest.param(..., marks=...).""" + for mark in item.iter_markers("parametrize"): + if len(mark.args) >= 2: + argvalues = mark.args[1] + else: + argvalues = mark.kwargs.get("argvalues", ()) + if isinstance(argvalues, str) or not hasattr(argvalues, "__iter__"): + continue + for val in argvalues: + marks = getattr(val, "marks", ()) + if not marks: + continue + if not isinstance(marks, (list, tuple)): + marks = (marks,) + for m in marks: + if getattr(m, "name", None) == "asyncio": + return m + return None + + # The function name needs to start with "pytest_" # see https://github.com/pytest-dev/pytest/issues/11307 @pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True) @@ -715,9 +736,9 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( updated_item = node if isinstance(node, Function): specialized_item_class = PytestAsyncioFunction.item_subclass_for(node) - if ( - specialized_item_class is not None - and _resolve_asyncio_marker(node) is not None + if specialized_item_class is not None and ( + _resolve_asyncio_marker(node) is not None + or _asyncio_marker_from_parametrize(node) is not None ): updated_item = specialized_item_class._from_function(node) updated_node_collection.append(updated_item) @@ -733,6 +754,8 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: return asyncio_marker = _resolve_asyncio_marker(metafunc.definition) + if asyncio_marker is None: + asyncio_marker = _asyncio_marker_from_parametrize(metafunc.definition) if asyncio_marker is None: return marker_loop_scope, marker_selected_factory_names = _parse_asyncio_marker( diff --git a/tests/test_loop_factory_parametrization.py b/tests/test_loop_factory_parametrization.py index 224c9141..e4c9a904 100644 --- a/tests/test_loop_factory_parametrization.py +++ b/tests/test_loop_factory_parametrization.py @@ -60,6 +60,36 @@ async def test_uses_custom_loop(): result.assert_outcomes(passed=1) +def test_loop_factories_apply_when_asyncio_mark_comes_from_parametrize( + pytester: Pytester, +) -> None: + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makeconftest(dedent("""\ + import asyncio + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + def pytest_asyncio_loop_factories(config, item): + return {"custom": CustomEventLoop} + """)) + pytester.makepyfile(dedent("""\ + import asyncio + import pytest + + pytest_plugins = "pytest_asyncio" + + @pytest.mark.parametrize( + "backend", + [pytest.param("asyncio", marks=pytest.mark.asyncio)], + ) + async def test_uses_custom_loop(backend): + assert type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" + """)) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + def test_named_hook_factories_parametrize_async_tests(pytester: Pytester) -> None: pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makeconftest(dedent("""\