Skip to content
Open
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/796.fixed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prevent parametrized ``event_loop_policy`` fixtures from parametrizing synchronous tests.
39 changes: 36 additions & 3 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -787,6 +794,32 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
)


def _add_fixture_to_metafunc(metafunc: pytest.Metafunc, fixture_name: str) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this changes pytest's internals and breaks valid usage. For example, on main, this passes.

$ cat >mre.py <<'# EOF'
import asyncio
import pytest


@pytest.fixture(params=[asyncio.DefaultEventLoopPolicy(), asyncio.DefaultEventLoopPolicy()])
def policy(request):
    return request.param


@pytest.fixture
def event_loop_policy(policy):
    return policy


@pytest.mark.asyncio
async def test_async():
    pass


async def test_sync():
    pass
# EOF

$ pytest mre.py
==================================================== test session starts ====================================================
platform darwin -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0
rootdir: /Users/tjkuson/workspace/pytest-asyncio
configfile: pyproject.toml
plugins: asyncio-1.4.0a3.dev24
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 2 items

mre.py EE                                                                                                             [100%]

========================================================== ERRORS ===========================================================
_______________________________________________ ERROR at setup of test_async ________________________________________________
The requested fixture has no parameter defined for test:
    mre.py::test_async

Requested fixture 'policy' defined in:
mre.py:6

Requested here:
.venv/lib/python3.13/site-packages/_pytest/fixtures.py:627
________________________________________________ ERROR at setup of test_sync ________________________________________________
The requested fixture has no parameter defined for test:
    mre.py::test_sync

Requested fixture 'policy' defined in:
mre.py:6

Requested here:
.venv/lib/python3.13/site-packages/_pytest/fixtures.py:627
===================================================== 2 errors in 0.02s =====================================================

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:
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:
Expand Down Expand Up @@ -1073,7 +1106,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()
Expand Down
92 changes: 92 additions & 0 deletions tests/markers/test_function_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,98 @@ 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_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,
):
Expand Down
2 changes: 0 additions & 2 deletions tests/test_set_event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading