Skip to content

Commit 323fadf

Browse files
tjkusonseifertm
authored andcommitted
Fix event loop leak on Python <3.14
1 parent 91b428e commit 323fadf

File tree

3 files changed

+52
-6
lines changed

3 files changed

+52
-6
lines changed

changelog.d/1373.fixed.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Fixed ``pytest_asyncio_loop_factories`` not installing the custom event loop as the current loop, and async fixture teardown/cache invalidation not being tied to the runner lifecycle, and sync ``@pytest_asyncio.fixture`` seeing the wrong event loop when multiple loop scopes are active.
1+
Fixed ``pytest_asyncio_loop_factories`` not installing the custom event loop as the current loop, async fixture teardown/cache invalidation not being tied to the runner lifecycle, sync ``@pytest_asyncio.fixture`` seeing the wrong event loop when multiple loop scopes are active, and an event loop leak on Python 3.10-3.13.

pytest_asyncio/plugin.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -794,12 +794,19 @@ def _temporary_event_loop(loop: AbstractEventLoop) -> Iterator[None]:
794794

795795

796796
@contextlib.contextmanager
797-
def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
797+
def _temporary_event_loop_policy(
798+
policy: AbstractEventLoopPolicy,
799+
*,
800+
has_custom_factory: bool,
801+
) -> Iterator[None]:
798802
old_loop_policy = _get_event_loop_policy()
799-
try:
800-
old_loop = _get_event_loop_no_warn()
801-
except RuntimeError:
803+
if has_custom_factory:
802804
old_loop = None
805+
else:
806+
try:
807+
old_loop = _get_event_loop_no_warn()
808+
except RuntimeError:
809+
old_loop = None
803810
_set_event_loop_policy(policy)
804811
try:
805812
yield
@@ -1012,7 +1019,10 @@ def _scoped_runner(
10121019
) -> Iterator[Runner]:
10131020
new_loop_policy = event_loop_policy
10141021
debug_mode = _get_asyncio_debug(request.config)
1015-
with _temporary_event_loop_policy(new_loop_policy):
1022+
with _temporary_event_loop_policy(
1023+
new_loop_policy,
1024+
has_custom_factory=_asyncio_loop_factory is not None,
1025+
):
10161026
runner = Runner(
10171027
debug=debug_mode,
10181028
loop_factory=_asyncio_loop_factory,

tests/test_loop_factory_parametrization.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,42 @@ async def test_uses_custom_loop():
533533
result.assert_outcomes(passed=1)
534534

535535

536+
def test_no_event_loop_leak_with_custom_factory(pytester: Pytester) -> None:
537+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
538+
pytester.makeconftest(dedent("""\
539+
import asyncio
540+
import pytest_asyncio
541+
542+
class CustomEventLoop(asyncio.SelectorEventLoop):
543+
pass
544+
545+
def pytest_asyncio_loop_factories(config, item):
546+
return {"custom": CustomEventLoop}
547+
548+
@pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session")
549+
async def session_fixture():
550+
yield
551+
552+
@pytest_asyncio.fixture(autouse=True)
553+
def sync_fixture():
554+
asyncio.get_event_loop()
555+
"""))
556+
pytester.makepyfile(dedent("""\
557+
import pytest
558+
559+
pytest_plugins = "pytest_asyncio"
560+
561+
@pytest.mark.asyncio
562+
async def test_passes():
563+
assert True
564+
"""))
565+
result = pytester.runpytest_subprocess(
566+
"--asyncio-mode=auto", "-W", "error::ResourceWarning"
567+
)
568+
result.assert_outcomes(passed=1)
569+
result.stderr.no_fnmatch_line("*unclosed event loop*")
570+
571+
536572
def test_function_loop_scope_allows_per_test_factories_with_session_default(
537573
pytester: Pytester,
538574
) -> None:

0 commit comments

Comments
 (0)