Skip to content

Commit d35f72e

Browse files
committed
Fix for Python 3.14
Python 3.14 has replaced the event loop policies with "runners", so that event loop policies now issue a deprecation warning. Ensure that the code works with runners and patches them too. Signed-off-by: Sergey Vasilyev <[email protected]>
1 parent b47ca11 commit d35f72e

File tree

1 file changed

+22
-5
lines changed

1 file changed

+22
-5
lines changed

looptime/plugin.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
from __future__ import annotations
109109

110110
import asyncio
111+
import sys
111112
import warnings
112113
from typing import Any
113114

@@ -166,7 +167,11 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
166167
result = yield
167168

168169
# Only do the magic if in the area of our interest & only for fixtures making the event loops.
169-
if _should_patch(fixturedef, request) and isinstance(result, asyncio.BaseEventLoop):
170+
# TODO: rewrite to simpler `if` & match-case when Python 3.10 is dropped (≈October 2026).
171+
should_patch = _should_patch(fixturedef, request)
172+
is_loop = isinstance(result, asyncio.BaseEventLoop)
173+
is_runner = False if sys.version_info < (3, 11) else isinstance(result, asyncio.Runner)
174+
if should_patch and (is_loop or is_runner):
170175

171176
# Populate the helper mapper of names-to-scopes, as used in the test hook below.
172177
if EVENT_LOOP_SCOPES not in request.session.stash:
@@ -179,7 +184,15 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
179184
# NB: For the lowest "function" scope, we still cannot decide which options to use, since
180185
# we do not know yet if it will be the running loop or not — so we cannot optimize here
181186
# in order to patch-and-configure only once; we must patch here & configure+activate later.
182-
result = patchers.patch_event_loop(result, _enabled=False)
187+
if isinstance(result, asyncio.BaseEventLoop):
188+
patchers.patch_event_loop(result, _enabled=False)
189+
elif sys.version_info >= (3, 11) and isinstance(result, asyncio.Runner):
190+
# Available only in python>=3.11, but mandatory for python>=3.14.
191+
# The runner of pytest-asyncio is already entered, which means the loop is created.
192+
# Even if not created, we cannot postpone the loop creation, so we create it here.
193+
loop = result.get_loop()
194+
if isinstance(loop, asyncio.BaseEventLoop):
195+
patchers.patch_event_loop(loop, _enabled=False)
183196

184197
return result
185198

@@ -188,7 +201,8 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
188201
def pytest_fixture_post_finalizer(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureRequest) -> Any:
189202
# Cleanup the helper mapper of the fixture's names-to-scopes, as used in the test-running hook.
190203
# Internal consistency check: some cases should not happen, but we do not fail if they do.
191-
if EVENT_LOOP_SCOPES in request.session.stash:
204+
should_patch = _should_patch(fixturedef, request)
205+
if should_patch and EVENT_LOOP_SCOPES in request.session.stash:
192206
event_loop_scopes: EventLoopScopes = request.session.stash[EVENT_LOOP_SCOPES]
193207
if fixturedef.argname not in event_loop_scopes:
194208
warnings.warn(
@@ -232,8 +246,11 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Any:
232246
funcargs: dict[str, Any] = pyfuncitem.funcargs
233247
if 'event_loop_policy' in funcargs: # pytest-asyncio>=1.0.0
234248
# This can be ANY event loop of ANY declared scope of pytest-asyncio.
235-
policy: asyncio.AbstractEventLoopPolicy = funcargs['event_loop_policy']
236-
running_loop = policy.get_event_loop()
249+
policy = funcargs['event_loop_policy']
250+
try:
251+
running_loop = policy.get_event_loop()
252+
except RuntimeError: # a sync test with no loop set? not our business!
253+
return (yield)
237254
elif 'event_loop' in funcargs: # pytest-asyncio<1.0.0
238255
# The hook itself has NO "running" loop — because it is sync, not async.
239256
running_loop = funcargs['event_loop']

0 commit comments

Comments
 (0)