Skip to content

Commit 174bddb

Browse files
authored
Explain new event loop in fixture finalizer (#486)
* [docs] Explain why a new event loop needs to be created after finalizing the event_loop fixture. Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de> * [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 <m.seifert@digitalernachschub.de> * [test] Added more variations to test which asserts that the event loop can be set to None. Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de> * [refactor] Renamed test_event_loop_scope.py to test_event_loop_fixture_finalizer.py. Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de> * [docs] Documented that pytest_fixture_post_finalizer may be called multiple times for any specific fixture. Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de> --------- Signed-off-by: Michael Seifert <m.seifert@digitalernachschub.de>
1 parent c4e082d commit 174bddb

File tree

3 files changed

+101
-40
lines changed

3 files changed

+101
-40
lines changed

pytest_asyncio/plugin.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,12 @@ def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
372372

373373
@pytest.hookimpl(trylast=True)
374374
def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -> None:
375-
"""Called after fixture teardown"""
375+
"""
376+
Called after fixture teardown.
377+
378+
Note that this function may be called multiple times for any specific fixture.
379+
see https://github.com/pytest-dev/pytest/issues/5848
380+
"""
376381
if fixturedef.argname == "event_loop":
377382
policy = asyncio.get_event_loop_policy()
378383
try:
@@ -382,8 +387,13 @@ def pytest_fixture_post_finalizer(fixturedef: FixtureDef, request: SubRequest) -
382387
if loop is not None:
383388
# Clean up existing loop to avoid ResourceWarnings
384389
loop.close()
385-
new_loop = policy.new_event_loop() # Replace existing event loop
386-
# Ensure subsequent calls to get_event_loop() succeed
390+
# At this point, the event loop for the current thread is closed.
391+
# When a user calls asyncio.get_event_loop(), they will get a closed loop.
392+
# In order to avoid this side effect from pytest-asyncio, we need to replace
393+
# the current loop with a fresh one.
394+
# Note that we cannot set the loop to None, because get_event_loop only creates
395+
# a new loop, when set_event_loop has not been called.
396+
new_loop = policy.new_event_loop()
387397
policy.set_event_loop(new_loop)
388398

389399

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from textwrap import dedent
2+
3+
from pytest import Pytester
4+
5+
6+
def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Pytester):
7+
pytester.makepyfile(
8+
dedent(
9+
"""\
10+
import asyncio
11+
12+
import pytest
13+
14+
loop = asyncio.get_event_loop_policy().get_event_loop()
15+
16+
@pytest.mark.asyncio
17+
async def test_1():
18+
# This async test runs in its own event loop
19+
global loop
20+
running_loop = asyncio.get_event_loop_policy().get_event_loop()
21+
# Make sure this test case received a different loop
22+
assert running_loop is not loop
23+
24+
def test_2():
25+
# Code outside of pytest-asyncio should not receive a "used" event loop
26+
current_loop = asyncio.get_event_loop_policy().get_event_loop()
27+
assert not current_loop.is_running()
28+
assert not current_loop.is_closed()
29+
"""
30+
)
31+
)
32+
result = pytester.runpytest("--asyncio-mode=strict")
33+
result.assert_outcomes(passed=2)
34+
35+
36+
def test_event_loop_fixture_finalizer_handles_loop_set_to_none_sync(
37+
pytester: Pytester,
38+
):
39+
pytester.makepyfile(
40+
dedent(
41+
"""\
42+
import asyncio
43+
44+
def test_sync(event_loop):
45+
asyncio.get_event_loop_policy().set_event_loop(None)
46+
"""
47+
)
48+
)
49+
result = pytester.runpytest("--asyncio-mode=strict")
50+
result.assert_outcomes(passed=1)
51+
52+
53+
def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_without_fixture(
54+
pytester: Pytester,
55+
):
56+
pytester.makepyfile(
57+
dedent(
58+
"""\
59+
import asyncio
60+
import pytest
61+
62+
@pytest.mark.asyncio
63+
async def test_async_without_explicit_fixture_request():
64+
asyncio.get_event_loop_policy().set_event_loop(None)
65+
"""
66+
)
67+
)
68+
result = pytester.runpytest("--asyncio-mode=strict")
69+
result.assert_outcomes(passed=1)
70+
71+
72+
def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_with_fixture(
73+
pytester: Pytester,
74+
):
75+
pytester.makepyfile(
76+
dedent(
77+
"""\
78+
import asyncio
79+
import pytest
80+
81+
@pytest.mark.asyncio
82+
async def test_async_with_explicit_fixture_request(event_loop):
83+
asyncio.get_event_loop_policy().set_event_loop(None)
84+
"""
85+
)
86+
)
87+
result = pytester.runpytest("--asyncio-mode=strict")
88+
result.assert_outcomes(passed=1)

tests/test_event_loop_scope.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)