Skip to content

Commit 6f33fed

Browse files
cstructseifertm
authored andcommitted
fix: Fixes a bug that caused module-scoped async fixtures to fail when reused in other modules.
Async fixture synchronizers now choose the event loop for the async fixutre at runtime rather than relying on collection-time information. This fixes #862.
1 parent f45aa18 commit 6f33fed

File tree

2 files changed

+78
-49
lines changed

2 files changed

+78
-49
lines changed

pytest_asyncio/plugin.py

+43-49
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,8 @@ def _preprocess_async_fixtures(
251251
or default_loop_scope
252252
or fixturedef.scope
253253
)
254-
if scope == "function":
255-
event_loop_fixture_id: Optional[str] = "event_loop"
256-
else:
257-
event_loop_node = _retrieve_scope_root(collector, scope)
258-
event_loop_fixture_id = event_loop_node.stash.get(
259-
# Type ignored because of non-optimal mypy inference.
260-
_event_loop_fixture_id, # type: ignore[arg-type]
261-
None,
262-
)
254+
if scope == "function" and "event_loop" not in fixturedef.argnames:
255+
fixturedef.argnames += ("event_loop",)
263256
_make_asyncio_fixture_function(func, scope)
264257
function_signature = inspect.signature(func)
265258
if "event_loop" in function_signature.parameters:
@@ -271,58 +264,35 @@ def _preprocess_async_fixtures(
271264
f"instead."
272265
)
273266
)
274-
assert event_loop_fixture_id
275-
_inject_fixture_argnames(
276-
fixturedef,
277-
event_loop_fixture_id,
278-
)
279-
_synchronize_async_fixture(
280-
fixturedef,
281-
event_loop_fixture_id,
282-
)
267+
if "request" not in fixturedef.argnames:
268+
fixturedef.argnames += ("request",)
269+
_synchronize_async_fixture(fixturedef)
283270
assert _is_asyncio_fixture_function(fixturedef.func)
284271
processed_fixturedefs.add(fixturedef)
285272

286273

287-
def _inject_fixture_argnames(
288-
fixturedef: FixtureDef, event_loop_fixture_id: str
289-
) -> None:
290-
"""
291-
Ensures that `request` and `event_loop` are arguments of the specified fixture.
292-
"""
293-
to_add = []
294-
for name in ("request", event_loop_fixture_id):
295-
if name not in fixturedef.argnames:
296-
to_add.append(name)
297-
if to_add:
298-
fixturedef.argnames += tuple(to_add)
299-
300-
301-
def _synchronize_async_fixture(
302-
fixturedef: FixtureDef, event_loop_fixture_id: str
303-
) -> None:
274+
def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
304275
"""
305276
Wraps the fixture function of an async fixture in a synchronous function.
306277
"""
307278
if inspect.isasyncgenfunction(fixturedef.func):
308-
_wrap_asyncgen_fixture(fixturedef, event_loop_fixture_id)
279+
_wrap_asyncgen_fixture(fixturedef)
309280
elif inspect.iscoroutinefunction(fixturedef.func):
310-
_wrap_async_fixture(fixturedef, event_loop_fixture_id)
281+
_wrap_async_fixture(fixturedef)
311282

312283

313284
def _add_kwargs(
314285
func: Callable[..., Any],
315286
kwargs: Dict[str, Any],
316-
event_loop_fixture_id: str,
317287
event_loop: asyncio.AbstractEventLoop,
318288
request: FixtureRequest,
319289
) -> Dict[str, Any]:
320290
sig = inspect.signature(func)
321291
ret = kwargs.copy()
322292
if "request" in sig.parameters:
323293
ret["request"] = request
324-
if event_loop_fixture_id in sig.parameters:
325-
ret[event_loop_fixture_id] = event_loop
294+
if "event_loop" in sig.parameters:
295+
ret["event_loop"] = event_loop
326296
return ret
327297

328298

@@ -345,17 +315,19 @@ def _perhaps_rebind_fixture_func(
345315
return func
346316

347317

348-
def _wrap_asyncgen_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None:
318+
def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
349319
fixture = fixturedef.func
350320

351321
@functools.wraps(fixture)
352322
def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
353323
unittest = fixturedef.unittest if hasattr(fixturedef, "unittest") else False
354324
func = _perhaps_rebind_fixture_func(fixture, request.instance, unittest)
355-
event_loop = kwargs.pop(event_loop_fixture_id)
356-
gen_obj = func(
357-
**_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request)
325+
event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
326+
request, func
358327
)
328+
event_loop = request.getfixturevalue(event_loop_fixture_id)
329+
kwargs.pop(event_loop_fixture_id, None)
330+
gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
359331

360332
async def setup():
361333
res = await gen_obj.__anext__()
@@ -383,26 +355,48 @@ async def async_finalizer() -> None:
383355
fixturedef.func = _asyncgen_fixture_wrapper
384356

385357

386-
def _wrap_async_fixture(fixturedef: FixtureDef, event_loop_fixture_id: str) -> None:
358+
def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
387359
fixture = fixturedef.func
388360

389361
@functools.wraps(fixture)
390362
def _async_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
391363
unittest = False if pytest.version_tuple >= (8, 2) else fixturedef.unittest
392364
func = _perhaps_rebind_fixture_func(fixture, request.instance, unittest)
393-
event_loop = kwargs.pop(event_loop_fixture_id)
365+
event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
366+
request, func
367+
)
368+
event_loop = request.getfixturevalue(event_loop_fixture_id)
369+
kwargs.pop(event_loop_fixture_id, None)
394370

395371
async def setup():
396-
res = await func(
397-
**_add_kwargs(func, kwargs, event_loop_fixture_id, event_loop, request)
398-
)
372+
res = await func(**_add_kwargs(func, kwargs, event_loop, request))
399373
return res
400374

401375
return event_loop.run_until_complete(setup())
402376

403377
fixturedef.func = _async_fixture_wrapper
404378

405379

380+
def _get_event_loop_fixture_id_for_async_fixture(
381+
request: FixtureRequest, func: Any
382+
) -> str:
383+
default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope")
384+
loop_scope = (
385+
getattr(func, "_loop_scope", None) or default_loop_scope or request.scope
386+
)
387+
if loop_scope == "function":
388+
event_loop_fixture_id = "event_loop"
389+
else:
390+
event_loop_node = _retrieve_scope_root(request._pyfuncitem, loop_scope)
391+
event_loop_fixture_id = event_loop_node.stash.get(
392+
# Type ignored because of non-optimal mypy inference.
393+
_event_loop_fixture_id, # type: ignore[arg-type]
394+
"",
395+
)
396+
assert event_loop_fixture_id
397+
return event_loop_fixture_id
398+
399+
406400
class PytestAsyncioFunction(Function):
407401
"""Base class for all test functions managed by pytest-asyncio."""
408402

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_asyncio_mark_provides_package_scoped_loop_strict_mode(pytester: Pytester):
7+
pytester.makepyfile(
8+
__init__="",
9+
conftest=dedent(
10+
"""\
11+
import pytest_asyncio
12+
@pytest_asyncio.fixture(scope="module")
13+
async def async_shared_module_fixture():
14+
return True
15+
"""
16+
),
17+
test_module_one=dedent(
18+
"""\
19+
import pytest
20+
@pytest.mark.asyncio
21+
async def test_shared_module_fixture_use_a(async_shared_module_fixture):
22+
assert async_shared_module_fixture is True
23+
"""
24+
),
25+
test_module_two=dedent(
26+
"""\
27+
import pytest
28+
@pytest.mark.asyncio
29+
async def test_shared_module_fixture_use_b(async_shared_module_fixture):
30+
assert async_shared_module_fixture is True
31+
"""
32+
),
33+
)
34+
result = pytester.runpytest("--asyncio-mode=strict")
35+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)