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/1501.fixed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The scoped runner is now registered as a dependency of managed fixtures. This ensures loop factory parameter changes tear down dependent fixtures in dependency order. (`#1501 <https://github.com/pytest-dev/pytest-asyncio/issues/1501>`_)
39 changes: 33 additions & 6 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,18 +328,29 @@ def pytest_report_header(config: Config) -> list[str]:


def _fixture_synchronizer(
fixturedef: FixtureDef, runner: Runner, request: FixtureRequest
fixturedef: FixtureDef,
runner: Runner,
request: FixtureRequest,
runner_fixture_id: str,
) -> Callable:
"""Returns a synchronous function evaluating the specified fixture."""
fixture_function = resolve_fixture_function(fixturedef, request)
if inspect.isasyncgenfunction(fixturedef.func):
return _wrap_asyncgen_fixture(fixture_function, runner, request) # type: ignore[arg-type]
return _wrap_asyncgen_fixture( # type: ignore[arg-type]
fixture_function, runner, request, runner_fixture_id
)
elif inspect.iscoroutinefunction(fixturedef.func):
return _wrap_async_fixture(fixture_function, runner, request) # type: ignore[arg-type]
return _wrap_async_fixture( # type: ignore[arg-type]
fixture_function, runner, request, runner_fixture_id
)
elif inspect.isgeneratorfunction(fixturedef.func):
return _wrap_syncgen_fixture(fixture_function, runner) # type: ignore[arg-type]
return _wrap_syncgen_fixture( # type: ignore[arg-type]
fixture_function, runner, runner_fixture_id
)
else:
return _wrap_sync_fixture(fixture_function, runner) # type: ignore[arg-type]
return _wrap_sync_fixture( # type: ignore[arg-type]
fixture_function, runner, runner_fixture_id
)


SyncGenFixtureParams = ParamSpec("SyncGenFixtureParams")
Expand All @@ -351,12 +362,14 @@ def _wrap_syncgen_fixture(
SyncGenFixtureParams, Generator[SyncGenFixtureYieldType]
],
runner: Runner,
runner_fixture_id: str,
) -> Callable[SyncGenFixtureParams, Generator[SyncGenFixtureYieldType]]:
@functools.wraps(fixture_function)
def _syncgen_fixture_wrapper(
*args: SyncGenFixtureParams.args,
**kwargs: SyncGenFixtureParams.kwargs,
) -> Generator[SyncGenFixtureYieldType]:
kwargs.pop(runner_fixture_id, None)
with _temporary_event_loop(runner.get_loop()):
yield from fixture_function(*args, **kwargs)

Expand All @@ -370,12 +383,14 @@ def _syncgen_fixture_wrapper(
def _wrap_sync_fixture(
fixture_function: Callable[SyncFixtureParams, SyncFixtureReturnType],
runner: Runner,
runner_fixture_id: str,
) -> Callable[SyncFixtureParams, SyncFixtureReturnType]:
@functools.wraps(fixture_function)
def _sync_fixture_wrapper(
*args: SyncFixtureParams.args,
**kwargs: SyncFixtureParams.kwargs,
) -> SyncFixtureReturnType:
kwargs.pop(runner_fixture_id, None)
with _temporary_event_loop(runner.get_loop()):
return fixture_function(*args, **kwargs)

Expand All @@ -392,12 +407,14 @@ def _wrap_asyncgen_fixture(
],
runner: Runner,
request: FixtureRequest,
runner_fixture_id: str,
) -> Callable[AsyncGenFixtureParams, AsyncGenFixtureYieldType]:
@functools.wraps(fixture_function)
def _asyncgen_fixture_wrapper(
*args: AsyncGenFixtureParams.args,
**kwargs: AsyncGenFixtureParams.kwargs,
):
kwargs.pop(runner_fixture_id, None)
gen_obj = fixture_function(*args, **kwargs)

async def setup():
Expand Down Expand Up @@ -442,12 +459,15 @@ def _wrap_async_fixture(
],
runner: Runner,
request: FixtureRequest,
runner_fixture_id: str,
) -> Callable[AsyncFixtureParams, AsyncFixtureReturnType]:
@functools.wraps(fixture_function)
def _async_fixture_wrapper(
*args: AsyncFixtureParams.args,
**kwargs: AsyncFixtureParams.kwargs,
):
kwargs.pop(runner_fixture_id, None)

async def setup():
res = await fixture_function(*args, **kwargs)
return res
Expand Down Expand Up @@ -932,13 +952,20 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
or fixturedef.scope
)
runner_fixture_id = f"_{loop_scope}_scoped_runner"
if runner_fixture_id not in fixturedef.argnames:
# pytest derives fixture dependencies from argnames. Add the runner here so
# its parametrization invalidates dependent async fixtures in dependency
# order, rather than finalizing a parent fixture before its child.
object.__setattr__(
fixturedef, "argnames", (*fixturedef.argnames, runner_fixture_id)
)
runner = request.getfixturevalue(runner_fixture_id)
# Prevent the runner closing before the fixture's async teardown.
runner_fixturedef = request._get_active_fixturedef(runner_fixture_id)
runner_fixturedef.addfinalizer(
functools.partial(fixturedef.finish, request=request)
)
synchronizer = _fixture_synchronizer(fixturedef, runner, request)
synchronizer = _fixture_synchronizer(fixturedef, runner, request, runner_fixture_id)
_make_asyncio_fixture_function(synchronizer, loop_scope)
with MonkeyPatch.context() as c:
c.setattr(fixturedef, "func", synchronizer)
Expand Down
35 changes: 35 additions & 0 deletions tests/test_loop_factory_parametrization.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,41 @@ async def test_async(request):
result.assert_outcomes(passed=3)


def test_sync_test_with_shared_async_fixture_is_not_torn_down(
pytester: Pytester,
) -> None:
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = session")
pytester.makeconftest(dedent("""\
import asyncio

def pytest_asyncio_loop_factories(config, item):
return {"default": asyncio.new_event_loop}
"""))
pytester.makepyfile(dedent("""\
import pytest
import pytest_asyncio

pytest_plugins = "pytest_asyncio"

@pytest_asyncio.fixture(scope="session")
async def parent():
yield "parent"

@pytest_asyncio.fixture(scope="session")
async def child(parent):
yield "child"

@pytest.mark.asyncio(loop_scope="session")
async def test_async(parent):
assert parent == "parent"

def test_sync(child):
assert child == "child"
"""))
result = pytester.runpytest("--asyncio-mode=strict")
result.assert_outcomes(passed=2)


@pytest.mark.parametrize(
"hook_body",
(
Expand Down
Loading