Skip to content

Commit 2b08c50

Browse files
committed
Fix fixtures seeing the wrong event loop
1 parent 7499c3d commit 2b08c50

File tree

3 files changed

+181
-2
lines changed

3 files changed

+181
-2
lines changed

changelog.d/1373.fixed.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Fixed ``pytest_asyncio_loop_factories`` not installing the custom event loop as the current loop, and async fixture teardown/cache invalidation not being tied to the runner lifecycle.
1+
Fixed ``pytest_asyncio_loop_factories`` not installing the custom event loop as the current loop, and async fixture teardown/cache invalidation not being tied to the runner lifecycle, and sync ``@pytest_asyncio.fixture`` seeing the wrong event loop when multiple loop scopes are active.

pytest_asyncio/plugin.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,50 @@ def _fixture_synchronizer(
328328
return _wrap_asyncgen_fixture(fixture_function, runner, request) # type: ignore[arg-type]
329329
elif inspect.iscoroutinefunction(fixturedef.func):
330330
return _wrap_async_fixture(fixture_function, runner, request) # type: ignore[arg-type]
331+
elif inspect.isgeneratorfunction(fixturedef.func):
332+
return _wrap_syncgen_fixture(fixture_function, runner) # type: ignore[arg-type]
331333
else:
332-
return fixturedef.func
334+
return _wrap_sync_fixture(fixture_function, runner) # type: ignore[arg-type]
335+
336+
337+
SyncGenFixtureParams = ParamSpec("SyncGenFixtureParams")
338+
SyncGenFixtureYieldType = TypeVar("SyncGenFixtureYieldType")
339+
340+
341+
def _wrap_syncgen_fixture(
342+
fixture_function: Callable[
343+
SyncGenFixtureParams, Generator[SyncGenFixtureYieldType]
344+
],
345+
runner: Runner,
346+
) -> Callable[SyncGenFixtureParams, Generator[SyncGenFixtureYieldType]]:
347+
@functools.wraps(fixture_function)
348+
def _syncgen_fixture_wrapper(
349+
*args: SyncGenFixtureParams.args,
350+
**kwargs: SyncGenFixtureParams.kwargs,
351+
) -> Generator[SyncGenFixtureYieldType]:
352+
with _temporary_event_loop(runner.get_loop()):
353+
yield from fixture_function(*args, **kwargs)
354+
355+
return _syncgen_fixture_wrapper
356+
357+
358+
SyncFixtureParams = ParamSpec("SyncFixtureParams")
359+
SyncFixtureReturnType = TypeVar("SyncFixtureReturnType")
360+
361+
362+
def _wrap_sync_fixture(
363+
fixture_function: Callable[SyncFixtureParams, SyncFixtureReturnType],
364+
runner: Runner,
365+
) -> Callable[SyncFixtureParams, SyncFixtureReturnType]:
366+
@functools.wraps(fixture_function)
367+
def _sync_fixture_wrapper(
368+
*args: SyncFixtureParams.args,
369+
**kwargs: SyncFixtureParams.kwargs,
370+
) -> SyncFixtureReturnType:
371+
with _temporary_event_loop(runner.get_loop()):
372+
return fixture_function(*args, **kwargs)
373+
374+
return _sync_fixture_wrapper
333375

334376

335377
AsyncGenFixtureParams = ParamSpec("AsyncGenFixtureParams")
@@ -729,6 +771,22 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
729771
)
730772

731773

774+
@contextlib.contextmanager
775+
def _temporary_event_loop(loop: AbstractEventLoop) -> Iterator[None]:
776+
try:
777+
old_loop = _get_event_loop_no_warn()
778+
except RuntimeError:
779+
old_loop = None
780+
if old_loop is loop:
781+
yield
782+
return
783+
_set_event_loop(loop)
784+
try:
785+
yield
786+
finally:
787+
_set_event_loop(old_loop)
788+
789+
732790
@contextlib.contextmanager
733791
def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
734792
old_loop_policy = _get_event_loop_policy()

tests/test_loop_factory_parametrization.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,127 @@ async def test_debug_mode_visible():
605605
result.assert_outcomes(passed=1)
606606

607607

608+
@pytest.mark.parametrize(
609+
("fixture_scope", "wider_scope"),
610+
[
611+
("function", "module"),
612+
("function", "package"),
613+
("function", "session"),
614+
("module", "session"),
615+
("package", "session"),
616+
],
617+
)
618+
def test_sync_fixture_sees_its_own_loop_when_wider_scoped_loop_active(
619+
pytester: Pytester,
620+
fixture_scope: str,
621+
wider_scope: str,
622+
) -> None:
623+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
624+
pytester.makeconftest(dedent(f"""\
625+
import asyncio
626+
import pytest_asyncio
627+
628+
class CustomEventLoop(asyncio.SelectorEventLoop):
629+
pass
630+
631+
def pytest_asyncio_loop_factories(config, item):
632+
return {{"custom": CustomEventLoop}}
633+
634+
@pytest_asyncio.fixture(
635+
autouse=True,
636+
scope="{wider_scope}",
637+
loop_scope="{wider_scope}",
638+
)
639+
async def wider_scoped_fixture():
640+
yield
641+
642+
@pytest_asyncio.fixture(
643+
autouse=True,
644+
scope="{fixture_scope}",
645+
loop_scope="{fixture_scope}",
646+
)
647+
def sync_fixture_captures_loop():
648+
return id(asyncio.get_event_loop())
649+
"""))
650+
pytester.makepyfile(dedent(f"""\
651+
import asyncio
652+
import pytest
653+
654+
pytest_plugins = "pytest_asyncio"
655+
656+
@pytest.mark.asyncio(loop_scope="{fixture_scope}")
657+
async def test_sync_fixture_and_test_see_same_loop(
658+
sync_fixture_captures_loop,
659+
):
660+
assert sync_fixture_captures_loop == id(
661+
asyncio.get_running_loop()
662+
)
663+
"""))
664+
result = pytester.runpytest("--asyncio-mode=strict")
665+
result.assert_outcomes(passed=1)
666+
667+
668+
@pytest.mark.parametrize(
669+
("fixture_scope", "wider_scope"),
670+
[
671+
("function", "module"),
672+
("function", "session"),
673+
("module", "session"),
674+
],
675+
)
676+
def test_sync_generator_fixture_teardown_sees_own_loop(
677+
pytester: Pytester,
678+
fixture_scope: str,
679+
wider_scope: str,
680+
) -> None:
681+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
682+
pytester.makeconftest(dedent(f"""\
683+
import asyncio
684+
import pytest_asyncio
685+
686+
class CustomEventLoop(asyncio.SelectorEventLoop):
687+
pass
688+
689+
def pytest_asyncio_loop_factories(config, item):
690+
return {{"custom": CustomEventLoop}}
691+
692+
@pytest_asyncio.fixture(
693+
autouse=True,
694+
scope="{wider_scope}",
695+
loop_scope="{wider_scope}",
696+
)
697+
async def wider_scoped_fixture():
698+
yield
699+
700+
@pytest_asyncio.fixture(
701+
autouse=True,
702+
scope="{fixture_scope}",
703+
loop_scope="{fixture_scope}",
704+
)
705+
def sync_generator_fixture():
706+
loop_at_setup = id(asyncio.get_event_loop())
707+
yield loop_at_setup
708+
loop_at_teardown = id(asyncio.get_event_loop())
709+
assert loop_at_setup == loop_at_teardown
710+
"""))
711+
pytester.makepyfile(dedent(f"""\
712+
import asyncio
713+
import pytest
714+
715+
pytest_plugins = "pytest_asyncio"
716+
717+
@pytest.mark.asyncio(loop_scope="{fixture_scope}")
718+
async def test_generator_fixture_sees_correct_loop(
719+
sync_generator_fixture,
720+
):
721+
assert sync_generator_fixture == id(
722+
asyncio.get_running_loop()
723+
)
724+
"""))
725+
result = pytester.runpytest("--asyncio-mode=strict")
726+
result.assert_outcomes(passed=1)
727+
728+
608729
@pytest.mark.parametrize("loop_scope", ("module", "package", "session"))
609730
def test_async_generator_fixture_teardown_runs_under_custom_factory(
610731
pytester: Pytester,

0 commit comments

Comments
 (0)