108108from __future__ import annotations
109109
110110import asyncio
111+ import sys
111112import warnings
112113from 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
188201def 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