Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1463.fixed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply configured loop factories when ``pytest.mark.asyncio`` is attached through ``pytest.param`` rather than on the test function itself.
29 changes: 26 additions & 3 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions tests/test_loop_factory_parametrization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("""\
Expand Down
Loading