1919- A single event loop can be shared by multiple (but not all) tests.
2020- A single test can be spread over multiple (but not all) event loops.
2121
22- An classic example:
22+ A classic example:
2323
2424- A session-scoped fixture ``server`` starts a port listener & an HTTP server.
2525- A module-scoped fixture ``data`` populates the server via POST requests.
5252 Previously, the higher-scoped fixtures did not exist, so nothing breaks.
5353
54542. If the start time is explicitly defined and is in the future, move the time
55- forwards as specified — indistinguishable from the previous behaviour
55+ forward as specified — indistinguishable from the previous behaviour
5656 (except there could be artifacts from the previous tests in the loop).
5757
58583. If the start time is explicitly defined and is in the past, issue a warning
@@ -157,10 +157,6 @@ def pytest_addoption(parser: Any) -> None:
157157 help = "Run unmarked tests with the fake loop time by default." )
158158
159159
160- EventLoopScopes = dict [str , list [str ]] # {fixture_name -> [outer_scopes, …, innermost_scope]}
161- EVENT_LOOP_SCOPES = pytest .StashKey [EventLoopScopes ]()
162-
163-
164160@pytest .hookimpl (wrapper = True )
165161def pytest_fixture_setup (fixturedef : pytest .FixtureDef [Any ], request : pytest .FixtureRequest ) -> Any :
166162 # Setup as usual. We do the magic only afterwards, when we have the event loop created.
@@ -182,19 +178,12 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
182178 else :
183179 is_bp_runner = isinstance (result , bp_Runner )
184180
181+ # Patch the event loop at creation — even if unused and not enabled. We cannot patch later
182+ # in the middle of the run: e.g. for a session-scoped loop used in a few tests out of many.
183+ # NB: For the lowest "function" scope, we still cannot decide which options to use, since
184+ # we do not know yet if it will be the running loop or not — so we cannot optimize here
185+ # in order to patch-and-configure only once; we must patch here & configure+activate later.
185186 if should_patch and (is_loop or is_runner or is_bp_runner ):
186-
187- # Populate the helper mapper of names-to-scopes, as used in the test hook below.
188- if EVENT_LOOP_SCOPES not in request .session .stash :
189- request .session .stash [EVENT_LOOP_SCOPES ] = {}
190- event_loop_scopes : EventLoopScopes = request .session .stash [EVENT_LOOP_SCOPES ]
191- event_loop_scopes .setdefault (fixturedef .argname , []).append (fixturedef .scope )
192-
193- # Patch the event loop at creation — even if unused and not enabled. We cannot patch later
194- # in the middle of the run: e.g. for a session-scoped loop used in a few tests out of many.
195- # NB: For the lowest "function" scope, we still cannot decide which options to use, since
196- # we do not know yet if it will be the running loop or not — so we cannot optimize here
197- # in order to patch-and-configure only once; we must patch here & configure+activate later.
198187 if isinstance (result , asyncio .BaseEventLoop ):
199188 patchers .patch_event_loop (result , _enabled = False )
200189 elif sys .version_info >= (3 , 11 ) and isinstance (result , asyncio .Runner ):
@@ -212,39 +201,6 @@ def pytest_fixture_setup(fixturedef: pytest.FixtureDef[Any], request: pytest.Fix
212201 return result
213202
214203
215- @pytest .hookimpl (wrapper = True )
216- def pytest_fixture_post_finalizer (fixturedef : pytest .FixtureDef [Any ], request : pytest .FixtureRequest ) -> Any :
217- # Cleanup the helper mapper of the fixture's names-to-scopes, as used in the test-running hook.
218- # Internal consistency check: some cases should not happen, but we do not fail if they do.
219- should_patch = _should_patch (fixturedef , request )
220- if should_patch and EVENT_LOOP_SCOPES in request .session .stash :
221- event_loop_scopes : EventLoopScopes = request .session .stash [EVENT_LOOP_SCOPES ]
222- if fixturedef .argname not in event_loop_scopes :
223- warnings .warn (
224- f"Fixture { fixturedef .argname !r} not found in the cache of scopes."
225- f" Report as a bug, please add a reproducible snippet." ,
226- RuntimeWarning ,
227- )
228- elif not event_loop_scopes [fixturedef .argname ]:
229- warnings .warn (
230- f"Fixture { fixturedef .argname !r} has the empty cache of scopes."
231- f" Report as a bug, please add a reproducible snippet." ,
232- RuntimeWarning ,
233- )
234- elif event_loop_scopes [fixturedef .argname ][- 1 ] != fixturedef .scope :
235- warnings .warn (
236- f"Fixture { fixturedef .argname !r} has the broken cache of scopes:"
237- f" { event_loop_scopes [fixturedef .argname ]!r} , expecting { fixturedef .scope !r} "
238- f" Report as a bug, please add a reproducible snippet." ,
239- RuntimeWarning ,
240- )
241- else :
242- event_loop_scopes [fixturedef .argname ][- 1 :] = []
243-
244- # Go as usual.
245- return (yield )
246-
247-
248204# This hook is the latest (deepest) possible entrypoint before diving into the test function itself,
249205# with all the fixtures executed earlier, so that their setup time is not taken into account.
250206# Here, we know the actual running loop (out of many) chosen by pytest-asyncio & its marks/configs.
@@ -307,6 +263,9 @@ def _should_patch(fixturedef: pytest.FixtureDef[Any], request: pytest.FixtureReq
307263 return True
308264
309265 # pytest-asyncio>=1.0.0 exposes several event loops, one per scope, all hidden in the module.
266+ # We patch BOTH the default implementation, AND all those dirty hacks that users might make.
267+ # NB: We also report True on unrelated fixtures, such as `unused_tcp_port_factory`, etc.
268+ # This has no effect: they will not pass the extra test on being patchable loops & runners.
310269 asyncio_plugin = request .config .pluginmanager .getplugin ("asyncio" ) # a module object
311270 asyncio_names : set [str ] = {
312271 name for name in dir (asyncio_plugin ) if _is_fixture (getattr (asyncio_plugin , name ))
0 commit comments