Skip to content

Commit 9a7248a

Browse files
committed
[fix] Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test.
The fixture setup of the "event_loop" fixture closes any existing loop. The existing loop could also belong to a pytest-asyncio scoped event loop fixture. This caused async generator fixtures using the scoped loop to raise a RuntimeError on teardown, because the scoped loop was closed before the fixture finalizer could not be run. In fact, everything after the async generation fixture's "yield" statement had no functioning event loop. The issue was addressed by adding a special attribute to the scoped event loops provided by pytest-asyncio. If this attribute is present, the setup code of the "event_loop" fixture will not close the loop. This allows keeping backwards compatibility for code that doesn't use scoped loops. It is assumed that the magic attribute can be removed after the deprecation period of event_loop_ fixture overrides. Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
1 parent 97b8eb2 commit 9a7248a

File tree

4 files changed

+70
-1
lines changed

4 files changed

+70
-1
lines changed

docs/source/reference/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Changelog
33
=========
44

5+
0.23.3 (UNRELEASED)
6+
===================
7+
- Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#708 <https://github.com/pytest-dev/pytest-asyncio/issues/708>`_
8+
9+
510
0.23.2 (2023-12-04)
611
===================
712
- Fixes a bug that caused an internal pytest error when collecting .txt files `#703 <https://github.com/pytest-dev/pytest-asyncio/issues/703>`_

pytest_asyncio/plugin.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ def scoped_event_loop(
600600
new_loop_policy = event_loop_policy
601601
with _temporary_event_loop_policy(new_loop_policy):
602602
loop = asyncio.new_event_loop()
603+
loop.__pytest_asyncio = True # type: ignore[attr-defined]
603604
asyncio.set_event_loop(loop)
604605
yield loop
605606
loop.close()
@@ -749,7 +750,8 @@ def pytest_fixture_setup(
749750
with warnings.catch_warnings():
750751
warnings.simplefilter("ignore", DeprecationWarning)
751752
old_loop = policy.get_event_loop()
752-
if old_loop is not loop:
753+
is_pytest_asyncio_loop = getattr(old_loop, "__pytest_asyncio", False)
754+
if old_loop is not loop and not is_pytest_asyncio_loop:
753755
old_loop.close()
754756
except RuntimeError:
755757
# Either the current event loop has been set to None
@@ -965,6 +967,7 @@ def _session_event_loop(
965967
new_loop_policy = event_loop_policy
966968
with _temporary_event_loop_policy(new_loop_policy):
967969
loop = asyncio.new_event_loop()
970+
loop.__pytest_asyncio = True # type: ignore[attr-defined]
968971
asyncio.set_event_loop(loop)
969972
yield loop
970973
loop.close()

tests/markers/test_module_scope.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,36 @@ async def test_runs_in_different_loop_as_fixture(async_fixture):
282282
result.assert_outcomes(passed=1)
283283

284284

285+
def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test(
286+
pytester: Pytester,
287+
):
288+
pytester.makepyfile(
289+
dedent(
290+
"""\
291+
import asyncio
292+
293+
import pytest
294+
import pytest_asyncio
295+
296+
loop: asyncio.AbstractEventLoop
297+
298+
@pytest_asyncio.fixture(scope="module")
299+
async def async_fixture():
300+
global loop
301+
loop = asyncio.get_running_loop()
302+
yield
303+
304+
@pytest.mark.asyncio(scope="function")
305+
async def test_runs_in_different_loop_as_fixture(async_fixture):
306+
global loop
307+
assert asyncio.get_running_loop() is not loop
308+
"""
309+
),
310+
)
311+
result = pytester.runpytest("--asyncio-mode=strict")
312+
result.assert_outcomes(passed=1)
313+
314+
285315
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
286316
pytester: Pytester,
287317
):

tests/markers/test_session_scope.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture):
350350
result.assert_outcomes(passed=1)
351351

352352

353+
def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test(
354+
pytester: Pytester,
355+
):
356+
pytester.makepyfile(
357+
__init__="",
358+
test_mixed_scopes=dedent(
359+
"""\
360+
import asyncio
361+
362+
import pytest
363+
import pytest_asyncio
364+
365+
loop: asyncio.AbstractEventLoop
366+
367+
@pytest_asyncio.fixture(scope="session")
368+
async def async_fixture():
369+
global loop
370+
loop = asyncio.get_running_loop()
371+
yield
372+
373+
@pytest.mark.asyncio
374+
async def test_runs_in_different_loop_as_fixture(async_fixture):
375+
global loop
376+
assert asyncio.get_running_loop() is not loop
377+
"""
378+
),
379+
)
380+
result = pytester.runpytest("--asyncio-mode=strict")
381+
result.assert_outcomes(passed=1)
382+
383+
353384
def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture(
354385
pytester: Pytester,
355386
):

0 commit comments

Comments
 (0)