diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index c02a528a..7e063d24 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +0.23.3 (UNRELEASED) +=================== +- 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 `_ + + 0.23.2 (2023-12-04) =================== - Fixes a bug that caused an internal pytest error when collecting .txt files `#703 `_ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 934fb91c..0a92730c 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -600,6 +600,7 @@ def scoped_event_loop( new_loop_policy = event_loop_policy with _temporary_event_loop_policy(new_loop_policy): loop = asyncio.new_event_loop() + loop.__pytest_asyncio = True # type: ignore[attr-defined] asyncio.set_event_loop(loop) yield loop loop.close() @@ -749,7 +750,8 @@ def pytest_fixture_setup( with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) old_loop = policy.get_event_loop() - if old_loop is not loop: + is_pytest_asyncio_loop = getattr(old_loop, "__pytest_asyncio", False) + if old_loop is not loop and not is_pytest_asyncio_loop: old_loop.close() except RuntimeError: # Either the current event loop has been set to None @@ -965,6 +967,7 @@ def _session_event_loop( new_loop_policy = event_loop_policy with _temporary_event_loop_policy(new_loop_policy): loop = asyncio.new_event_loop() + loop.__pytest_asyncio = True # type: ignore[attr-defined] asyncio.set_event_loop(loop) yield loop loop.close() diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 94547e40..cf6b2f60 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -282,6 +282,36 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): result.assert_outcomes(passed=1) +def test_allows_combining_module_scoped_asyncgen_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="module") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + yield + + @pytest.mark.asyncio(scope="function") + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ): diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index ac70d01d..bd0baee5 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -350,6 +350,37 @@ async def test_runs_in_different_loop_as_fixture(async_fixture): result.assert_outcomes(passed=1) +def test_allows_combining_session_scoped_asyncgen_fixture_with_function_scoped_test( + pytester: Pytester, +): + pytester.makepyfile( + __init__="", + test_mixed_scopes=dedent( + """\ + import asyncio + + import pytest + import pytest_asyncio + + loop: asyncio.AbstractEventLoop + + @pytest_asyncio.fixture(scope="session") + async def async_fixture(): + global loop + loop = asyncio.get_running_loop() + yield + + @pytest.mark.asyncio + async def test_runs_in_different_loop_as_fixture(async_fixture): + global loop + assert asyncio.get_running_loop() is not loop + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + def test_asyncio_mark_handles_missing_event_loop_triggered_by_fixture( pytester: Pytester, ):