@@ -319,12 +319,27 @@ def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
319319 kwargs .pop (event_loop_fixture_id , None )
320320 gen_obj = func (** _add_kwargs (func , kwargs , event_loop , request ))
321321
322- context = _event_loop_context .get (None )
323-
324322 async def setup ():
325323 res = await gen_obj .__anext__ () # type: ignore[union-attr]
326324 return res
327325
326+ context = contextvars .copy_context ()
327+ setup_task = _create_task_in_context (event_loop , setup (), context )
328+ result = event_loop .run_until_complete (setup_task )
329+
330+ # Copy the context vars set by the setup task back into the ambient
331+ # context for the test.
332+ context_tokens = []
333+ for var in context :
334+ try :
335+ if var .get () is context .get (var ):
336+ # Not modified by the fixture, so leave it as-is.
337+ continue
338+ except LookupError :
339+ pass
340+ token = var .set (context .get (var ))
341+ context_tokens .append ((var , token ))
342+
328343 def finalizer () -> None :
329344 """Yield again, to finalize."""
330345
@@ -341,14 +356,39 @@ async def async_finalizer() -> None:
341356 task = _create_task_in_context (event_loop , async_finalizer (), context )
342357 event_loop .run_until_complete (task )
343358
344- setup_task = _create_task_in_context (event_loop , setup (), context )
345- result = event_loop .run_until_complete (setup_task )
359+ # Since the fixture is now complete, restore any context variables
360+ # it had set back to their original values.
361+ while context_tokens :
362+ (var , token ) = context_tokens .pop ()
363+ var .reset (token )
364+
346365 request .addfinalizer (finalizer )
347366 return result
348367
349368 fixturedef .func = _asyncgen_fixture_wrapper # type: ignore[misc]
350369
351370
371+ def _create_task_in_context (loop , coro , context ):
372+ """
373+ Return an asyncio task that runs the coro in the specified context,
374+ if possible.
375+
376+ This allows fixture setup and teardown to be run as separate asyncio tasks,
377+ while still being able to use context-manager idioms to maintain context
378+ variables and make those variables visible to test functions.
379+
380+ This is only fully supported on Python 3.11 and newer, as it requires
381+ the API added for https://github.com/python/cpython/issues/91150.
382+ On earlier versions, the returned task will use the default context instead.
383+ """
384+ if context is not None :
385+ try :
386+ return loop .create_task (coro , context = context )
387+ except TypeError :
388+ pass
389+ return loop .create_task (coro )
390+
391+
352392def _wrap_async_fixture (fixturedef : FixtureDef ) -> None :
353393 fixture = fixturedef .func
354394
@@ -365,10 +405,11 @@ async def setup():
365405 res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
366406 return res
367407
368- task = _create_task_in_context (
369- event_loop , setup (), _event_loop_context .get (None )
370- )
371- return event_loop .run_until_complete (task )
408+ # Since the fixture doesn't have a cleanup phase, if it set any context
409+ # variables we don't have a good way to clear them again.
410+ # Instead, treat this fixture like an asyncio.Task, which has its own
411+ # independent Context that doesn't affect the caller.
412+ return event_loop .run_until_complete (setup ())
372413
373414 fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
374415
@@ -592,46 +633,6 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
592633 Session : "session" ,
593634}
594635
595- # _event_loop_context stores the Context in which asyncio tasks on the fixture
596- # event loop should be run. After fixture setup, individual async test functions
597- # are run on copies of this context.
598- _event_loop_context : contextvars .ContextVar [contextvars .Context ] = (
599- contextvars .ContextVar ("pytest_asyncio_event_loop_context" )
600- )
601-
602-
603- @contextlib .contextmanager
604- def _set_event_loop_context ():
605- """Set event_loop_context to a copy of the calling thread's current context."""
606- context = contextvars .copy_context ()
607- token = _event_loop_context .set (context )
608- try :
609- yield
610- finally :
611- _event_loop_context .reset (token )
612-
613-
614- def _create_task_in_context (loop , coro , context ):
615- """
616- Return an asyncio task that runs the coro in the specified context,
617- if possible.
618-
619- This allows fixture setup and teardown to be run as separate asyncio tasks,
620- while still being able to use context-manager idioms to maintain context
621- variables and make those variables visible to test functions.
622-
623- This is only fully supported on Python 3.11 and newer, as it requires
624- the API added for https://github.com/python/cpython/issues/91150.
625- On earlier versions, the returned task will use the default context instead.
626- """
627- if context is not None :
628- try :
629- return loop .create_task (coro , context = context )
630- except TypeError :
631- pass
632- return loop .create_task (coro )
633-
634-
635636# A stack used to push package-scoped loops during collection of a package
636637# and pop those loops during collection of a Module
637638__package_loop_stack : list [FixtureFunctionMarker | FixtureFunction ] = []
@@ -679,8 +680,7 @@ def scoped_event_loop(
679680 loop = asyncio .new_event_loop ()
680681 loop .__pytest_asyncio = True # type: ignore[attr-defined]
681682 asyncio .set_event_loop (loop )
682- with _set_event_loop_context ():
683- yield loop
683+ yield loop
684684 loop .close ()
685685
686686 # @pytest.fixture does not register the fixture anywhere, so pytest doesn't
@@ -987,16 +987,9 @@ def wrap_in_sync(
987987
988988 @functools .wraps (func )
989989 def inner (* args , ** kwargs ):
990- # Give each test its own context based on the loop's main context.
991- context = _event_loop_context .get (None )
992- if context is not None :
993- # We are using our own event loop fixture, so make a new copy of the
994- # fixture context so that the test won't pollute it.
995- context = context .copy ()
996-
997990 coro = func (* args , ** kwargs )
998991 _loop = _get_event_loop_no_warn ()
999- task = _create_task_in_context ( _loop , coro , context )
992+ task = asyncio . ensure_future ( coro , loop = _loop )
1000993 try :
1001994 _loop .run_until_complete (task )
1002995 except BaseException :
@@ -1105,8 +1098,7 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
11051098 # The magic value must be set as part of the function definition, because pytest
11061099 # seems to have multiple instances of the same FixtureDef or fixture function
11071100 loop .__original_fixture_loop = True # type: ignore[attr-defined]
1108- with _set_event_loop_context ():
1109- yield loop
1101+ yield loop
11101102 loop .close ()
11111103
11121104
@@ -1119,8 +1111,7 @@ def _session_event_loop(
11191111 loop = asyncio .new_event_loop ()
11201112 loop .__pytest_asyncio = True # type: ignore[attr-defined]
11211113 asyncio .set_event_loop (loop )
1122- with _set_event_loop_context ():
1123- yield loop
1114+ yield loop
11241115 loop .close ()
11251116
11261117
0 commit comments