Skip to content

Commit 0642dcd

Browse files
committed
fix: Fix broken event loop when a function-scoped test is in between two wider-scoped tests.
The event_loop fixture finalizers only close event loops that were not created by pytest-asyncio. This prevents the finalizers from accidentally closing a module-scoped loop, for example.
1 parent 050a5f8 commit 0642dcd

File tree

4 files changed

+76
-16
lines changed

4 files changed

+76
-16
lines changed

docs/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.25.1 (UNRELEASED)
6+
===================
7+
- Fixes an issue that caused a broken event loop when a function-scoped test was executed in between two tests with wider loop scope `#950 <https://github.com/pytest-dev/pytest-asyncio/issues/950>`_
8+
9+
510
0.25.0 (2024-12-13)
611
===================
712
- Deprecated: Added warning when asyncio test requests async ``@pytest.fixture`` in strict mode. This will become an error in a future version of flake8-asyncio. `#979 <https://github.com/pytest-dev/pytest-asyncio/pull/979>`_

pytest_asyncio/plugin.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import inspect
1111
import socket
1212
import warnings
13-
from asyncio import AbstractEventLoopPolicy
13+
from asyncio import AbstractEventLoop, AbstractEventLoopPolicy
1414
from collections.abc import (
1515
AsyncIterator,
1616
Awaitable,
@@ -762,6 +762,19 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No
762762
try:
763763
yield
764764
finally:
765+
# Try detecting user-created event loops that were left unclosed
766+
# at the end of a test.
767+
try:
768+
current_loop: AbstractEventLoop | None = _get_event_loop_no_warn()
769+
except RuntimeError:
770+
current_loop = None
771+
if current_loop is not None and not current_loop.is_closed():
772+
warnings.warn(
773+
_UNCLOSED_EVENT_LOOP_WARNING % current_loop,
774+
DeprecationWarning,
775+
)
776+
current_loop.close()
777+
765778
asyncio.set_event_loop_policy(old_loop_policy)
766779
# When a test uses both a scoped event loop and the event_loop fixture,
767780
# the "_provide_clean_event_loop" finalizer of the event_loop fixture
@@ -906,7 +919,7 @@ def _close_event_loop() -> None:
906919
loop = policy.get_event_loop()
907920
except RuntimeError:
908921
loop = None
909-
if loop is not None:
922+
if loop is not None and not getattr(loop, "__pytest_asyncio", False):
910923
if not loop.is_closed():
911924
warnings.warn(
912925
_UNCLOSED_EVENT_LOOP_WARNING % loop,
@@ -923,7 +936,7 @@ def _restore_policy():
923936
loop = _get_event_loop_no_warn(previous_policy)
924937
except RuntimeError:
925938
loop = None
926-
if loop:
939+
if loop and not getattr(loop, "__pytest_asyncio", False):
927940
loop.close()
928941
asyncio.set_event_loop_policy(previous_policy)
929942

@@ -938,8 +951,13 @@ def _provide_clean_event_loop() -> None:
938951
# Note that we cannot set the loop to None, because get_event_loop only creates
939952
# a new loop, when set_event_loop has not been called.
940953
policy = asyncio.get_event_loop_policy()
941-
new_loop = policy.new_event_loop()
942-
policy.set_event_loop(new_loop)
954+
try:
955+
old_loop = _get_event_loop_no_warn(policy)
956+
except RuntimeError:
957+
old_loop = None
958+
if old_loop is not None and not getattr(old_loop, "__pytest_asyncio", False):
959+
new_loop = policy.new_event_loop()
960+
policy.set_event_loop(new_loop)
943961

944962

945963
def _get_event_loop_no_warn(
@@ -1122,16 +1140,16 @@ def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
11221140
def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
11231141
"""Create an instance of the default event loop for each test case."""
11241142
new_loop_policy = request.getfixturevalue(event_loop_policy.__name__)
1125-
asyncio.set_event_loop_policy(new_loop_policy)
1126-
loop = asyncio.get_event_loop_policy().new_event_loop()
1127-
# Add a magic value to the event loop, so pytest-asyncio can determine if the
1128-
# event_loop fixture was overridden. Other implementations of event_loop don't
1129-
# set this value.
1130-
# The magic value must be set as part of the function definition, because pytest
1131-
# seems to have multiple instances of the same FixtureDef or fixture function
1132-
loop.__original_fixture_loop = True # type: ignore[attr-defined]
1133-
yield loop
1134-
loop.close()
1143+
with _temporary_event_loop_policy(new_loop_policy):
1144+
loop = asyncio.get_event_loop_policy().new_event_loop()
1145+
# Add a magic value to the event loop, so pytest-asyncio can determine if the
1146+
# event_loop fixture was overridden. Other implementations of event_loop don't
1147+
# set this value.
1148+
# The magic value must be set as part of the function definition, because pytest
1149+
# seems to have multiple instances of the same FixtureDef or fixture function
1150+
loop.__original_fixture_loop = True # type: ignore[attr-defined]
1151+
yield loop
1152+
loop.close()
11351153

11361154

11371155
@pytest.fixture(scope="session")

tests/markers/test_mixed_scope.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from textwrap import dedent
4+
5+
from pytest import Pytester
6+
7+
8+
def test_function_scoped_loop_restores_previous_loop_scope(pytester: Pytester):
9+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
10+
pytester.makepyfile(
11+
dedent(
12+
"""\
13+
import asyncio
14+
import pytest
15+
16+
17+
module_loop: asyncio.AbstractEventLoop
18+
19+
@pytest.mark.asyncio(loop_scope="module")
20+
async def test_remember_loop():
21+
global module_loop
22+
module_loop = asyncio.get_running_loop()
23+
24+
@pytest.mark.asyncio(loop_scope="function")
25+
async def test_with_function_scoped_loop():
26+
pass
27+
28+
@pytest.mark.asyncio(loop_scope="module")
29+
async def test_runs_in_same_loop():
30+
global module_loop
31+
assert asyncio.get_running_loop() is module_loop
32+
"""
33+
)
34+
)
35+
result = pytester.runpytest("--asyncio-mode=strict")
36+
result.assert_outcomes(passed=3)

tests/test_event_loop_fixture_finalizer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Py
1414
1515
import pytest
1616
17-
loop = asyncio.get_event_loop_policy().get_event_loop()
17+
loop = asyncio.new_event_loop()
18+
asyncio.set_event_loop(loop)
1819
1920
@pytest.mark.asyncio
2021
async def test_1():

0 commit comments

Comments
 (0)