Skip to content

Commit 8b8e6e9

Browse files
committed
refactor: Async fixtures are synchronized on demand rather than statically.
Previously, async coroutines and async generators used as fixture functions were wrapped with a fixture synchronizer during collection time. This allowed fixture function to be run as synchronous functions. This patch changes the synchronization to occur during the pytest_fixture_setup hook. The synchronization is now temporary, which means the wrapper fixture function is restored after the fixture setup has finished.
1 parent 8432333 commit 8b8e6e9

File tree

1 file changed

+27
-50
lines changed

1 file changed

+27
-50
lines changed

pytest_asyncio/plugin.py

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636
import pytest
3737
from _pytest.scope import Scope
3838
from pytest import (
39-
Collector,
4039
Config,
4140
FixtureDef,
4241
FixtureRequest,
4342
Function,
4443
Item,
4544
Mark,
4645
Metafunc,
46+
MonkeyPatch,
4747
Parser,
4848
PytestCollectionWarning,
4949
PytestDeprecationWarning,
@@ -231,39 +231,6 @@ def pytest_report_header(config: Config) -> list[str]:
231231
]
232232

233233

234-
def _preprocess_async_fixtures(
235-
collector: Collector,
236-
processed_fixturedefs: set[FixtureDef],
237-
) -> None:
238-
config = collector.config
239-
default_loop_scope = config.getini("asyncio_default_fixture_loop_scope")
240-
asyncio_mode = _get_asyncio_mode(config)
241-
fixturemanager = config.pluginmanager.get_plugin("funcmanage")
242-
assert fixturemanager is not None
243-
for fixtures in fixturemanager._arg2fixturedefs.values():
244-
for fixturedef in fixtures:
245-
func = fixturedef.func
246-
if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen(
247-
func
248-
):
249-
continue
250-
if asyncio_mode == Mode.STRICT and not _is_asyncio_fixture_function(func):
251-
# Ignore async fixtures without explicit asyncio mark in strict mode
252-
# This applies to pytest_trio fixtures, for example
253-
continue
254-
loop_scope = (
255-
getattr(func, "_loop_scope", None)
256-
or default_loop_scope
257-
or fixturedef.scope
258-
)
259-
_make_asyncio_fixture_function(func, loop_scope)
260-
if "request" not in fixturedef.argnames:
261-
fixturedef.argnames += ("request",)
262-
fixturedef.func = _fixture_synchronizer(fixturedef) # type: ignore[misc]
263-
assert _is_asyncio_fixture_function(fixturedef.func)
264-
processed_fixturedefs.add(fixturedef)
265-
266-
267234
def _fixture_synchronizer(fixturedef: FixtureDef) -> Callable:
268235
"""Returns a synchronous function evaluating the specified fixture."""
269236
if inspect.isasyncgenfunction(fixturedef.func):
@@ -599,22 +566,6 @@ def runtest(self) -> None:
599566
super().runtest()
600567

601568

602-
_HOLDER: set[FixtureDef] = set()
603-
604-
605-
# The function name needs to start with "pytest_"
606-
# see https://github.com/pytest-dev/pytest/issues/11307
607-
@pytest.hookimpl(specname="pytest_pycollect_makeitem", tryfirst=True)
608-
def pytest_pycollect_makeitem_preprocess_async_fixtures(
609-
collector: pytest.Module | pytest.Class, name: str, obj: object
610-
) -> pytest.Item | pytest.Collector | list[pytest.Item | pytest.Collector] | None:
611-
"""A pytest hook to collect asyncio coroutines."""
612-
if not collector.funcnamefilter(name):
613-
return None
614-
_preprocess_async_fixtures(collector, _HOLDER)
615-
return None
616-
617-
618569
# The function name needs to start with "pytest_"
619570
# see https://github.com/pytest-dev/pytest/issues/11307
620571
@pytest.hookimpl(specname="pytest_pycollect_makeitem", hookwrapper=True)
@@ -829,6 +780,32 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
829780
)
830781

831782

783+
@pytest.hookimpl(wrapper=True)
784+
def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
785+
asyncio_mode = _get_asyncio_mode(request.config)
786+
if not _is_asyncio_fixture_function(fixturedef.func):
787+
if asyncio_mode == Mode.STRICT:
788+
# Ignore async fixtures without explicit asyncio mark in strict mode
789+
# This applies to pytest_trio fixtures, for example
790+
return (yield)
791+
if not _is_coroutine_or_asyncgen(fixturedef.func):
792+
return (yield)
793+
default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope")
794+
loop_scope = (
795+
getattr(fixturedef.func, "_loop_scope", None)
796+
or default_loop_scope
797+
or fixturedef.scope
798+
)
799+
synchronizer = _fixture_synchronizer(fixturedef)
800+
_make_asyncio_fixture_function(synchronizer, loop_scope)
801+
with MonkeyPatch.context() as c:
802+
if "request" not in fixturedef.argnames:
803+
c.setattr(fixturedef, "argnames", (*fixturedef.argnames, "request"))
804+
c.setattr(fixturedef, "func", synchronizer)
805+
hook_result = yield
806+
return hook_result
807+
808+
832809
_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR = """\
833810
An asyncio pytest marker defines both "scope" and "loop_scope", \
834811
but it should only use "loop_scope".

0 commit comments

Comments
 (0)