From 93ec42a5682196e7672f7c4985ec07830c98a7de Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 20 Jan 2023 11:21:22 +0100 Subject: [PATCH 1/5] [docs] Explain why a new event loop needs to be created after finalizing the event_loop fixture. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f663ff68..31c314a1 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -382,8 +382,13 @@ def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) - if loop is not None: # Clean up existing loop to avoid ResourceWarnings loop.close() - new_loop = policy.new_event_loop() # Replace existing event loop - # Ensure subsequent calls to get_event_loop() succeed + # At this point, the event loop for the current thread is closed. + # When a user calls asyncio.get_event_loop(), they will get a closed loop. + # In order to avoid this side effect from pytest-asyncio, we need to replace + # the current loop with a fresh one. + # Note that we cannot set the loop to None, because get_event_loop only creates + # a new loop, when set_event_loop has not been called. + new_loop = policy.new_event_loop() policy.set_event_loop(new_loop) From 767e34c0fc6fd4ce9fbc0cd9ec915149796efd5b Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 20 Jan 2023 11:30:19 +0100 Subject: [PATCH 2/5] [refactor] Restructured tests for event_loop fixture finalizer. The tests were split up so that the name of the test case reflects the test purpose. Signed-off-by: Michael Seifert --- tests/test_event_loop_scope.py | 85 +++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/tests/test_event_loop_scope.py b/tests/test_event_loop_scope.py index 21fd6415..0b3f5204 100644 --- a/tests/test_event_loop_scope.py +++ b/tests/test_event_loop_scope.py @@ -1,37 +1,48 @@ -"""Test the event loop fixture provides a separate loop for each test. - -These tests need to be run together. -""" -import asyncio - -import pytest - -loop: asyncio.AbstractEventLoop - - -def test_1(): - global loop - # The main thread should have a default event loop. - loop = asyncio.get_event_loop_policy().get_event_loop() - - -@pytest.mark.asyncio -async def test_2(): - global loop - running_loop = asyncio.get_event_loop_policy().get_event_loop() - # Make sure this test case received a different loop - assert running_loop is not loop - loop = running_loop # Store the loop reference for later - - -def test_3(): - global loop - current_loop = asyncio.get_event_loop_policy().get_event_loop() - # Now the event loop from test_2 should have been cleaned up - assert loop is not current_loop - - -def test_4(event_loop): - # If a test sets the loop to None -- pytest_fixture_post_finalizer() - # still should work - asyncio.get_event_loop_policy().set_event_loop(None) +from textwrap import dedent + +from pytest import Pytester + + +def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + + import pytest + + loop = asyncio.get_event_loop_policy().get_event_loop() + + @pytest.mark.asyncio + async def test_1(): + # This async test runs in its own event loop + global loop + running_loop = asyncio.get_event_loop_policy().get_event_loop() + # Make sure this test case received a different loop + assert running_loop is not loop + + def test_2(): + # Code outside of pytest-asyncio should not receive a "used" event loop + current_loop = asyncio.get_event_loop_policy().get_event_loop() + assert not current_loop.is_running() + assert not current_loop.is_closed() + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=2) + + +def test_event_loop_fixture_finalizer_can_handle_loop_set_to_none(pytester: Pytester): + pytester.makepyfile( + dedent( + """\ + import asyncio + + def test_anything(event_loop): + asyncio.get_event_loop_policy().set_event_loop(None) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) From de68469552e0c48f2157aec2fdc6a684d8d08882 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 20 Jan 2023 11:40:52 +0100 Subject: [PATCH 3/5] [test] Added more variations to test which asserts that the event loop can be set to None. Signed-off-by: Michael Seifert --- tests/test_event_loop_scope.py | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/test_event_loop_scope.py b/tests/test_event_loop_scope.py index 0b3f5204..d3622a08 100644 --- a/tests/test_event_loop_scope.py +++ b/tests/test_event_loop_scope.py @@ -33,13 +33,53 @@ def test_2(): result.assert_outcomes(passed=2) -def test_event_loop_fixture_finalizer_can_handle_loop_set_to_none(pytester: Pytester): +def test_event_loop_fixture_finalizer_handles_loop_set_to_none_sync( + pytester: Pytester, +): pytester.makepyfile( dedent( """\ import asyncio - def test_anything(event_loop): + def test_sync(event_loop): + asyncio.get_event_loop_policy().set_event_loop(None) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_without_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio + async def test_async_without_explicit_fixture_request(): + asyncio.get_event_loop_policy().set_event_loop(None) + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + + +def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_with_fixture( + pytester: Pytester, +): + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest + + @pytest.mark.asyncio + async def test_async_with_explicit_fixture_request(event_loop): asyncio.get_event_loop_policy().set_event_loop(None) """ ) From bd30813e2a546ee010139ee1318be65be7d1964e Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 20 Jan 2023 11:41:45 +0100 Subject: [PATCH 4/5] [refactor] Renamed test_event_loop_scope.py to test_event_loop_fixture_finalizer.py. Signed-off-by: Michael Seifert --- ...t_event_loop_scope.py => test_event_loop_fixture_finalizer.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_event_loop_scope.py => test_event_loop_fixture_finalizer.py} (100%) diff --git a/tests/test_event_loop_scope.py b/tests/test_event_loop_fixture_finalizer.py similarity index 100% rename from tests/test_event_loop_scope.py rename to tests/test_event_loop_fixture_finalizer.py From 0e041e6d21cbb438849ce53617ac658a7a5c6776 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Fri, 20 Jan 2023 11:45:32 +0100 Subject: [PATCH 5/5] [docs] Documented that pytest_fixture_post_finalizer may be called multiple times for any specific fixture. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 31c314a1..21809b6d 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -372,7 +372,12 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool: @pytest.hookimpl(trylast=True) def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -> None: - """Called after fixture teardown""" + """ + Called after fixture teardown. + + Note that this function may be called multiple times for any specific fixture. + see https://github.com/pytest-dev/pytest/issues/5848 + """ if fixturedef.argname == "event_loop": policy = asyncio.get_event_loop_policy() try: