@@ -327,18 +327,7 @@ async def setup():
327327 setup_task = _create_task_in_context (event_loop , setup (), context )
328328 result = event_loop .run_until_complete (setup_task )
329329
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 ))
330+ reset_contextvars = _apply_contextvar_changes (context )
342331
343332 def finalizer () -> None :
344333 """Yield again, to finalize."""
@@ -355,38 +344,15 @@ async def async_finalizer() -> None:
355344
356345 task = _create_task_in_context (event_loop , async_finalizer (), context )
357346 event_loop .run_until_complete (task )
358-
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 )
347+ if reset_contextvars is not None :
348+ reset_contextvars ()
364349
365350 request .addfinalizer (finalizer )
366351 return result
367352
368353 fixturedef .func = _asyncgen_fixture_wrapper # type: ignore[misc]
369354
370355
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- try :
385- return loop .create_task (coro , context = context )
386- except TypeError :
387- return loop .create_task (coro )
388-
389-
390356def _wrap_async_fixture (fixturedef : FixtureDef ) -> None :
391357 fixture = fixturedef .func
392358
@@ -403,11 +369,23 @@ async def setup():
403369 res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
404370 return res
405371
406- # Since the fixture doesn't have a cleanup phase, if it set any context
407- # variables we don't have a good way to clear them again.
408- # Instead, treat this fixture like an asyncio.Task, which has its own
409- # independent Context that doesn't affect the caller.
410- return event_loop .run_until_complete (setup ())
372+ context = contextvars .copy_context ()
373+ setup_task = _create_task_in_context (event_loop , setup (), context )
374+ result = event_loop .run_until_complete (setup_task )
375+
376+ # Copy the context vars modified by the setup task into the current
377+ # context, and (if needed) add a finalizer to reset them.
378+ #
379+ # Note that this is slightly different from the behavior of a non-async
380+ # fixture, which would rely on the fixture author to add a finalizer
381+ # to reset the variables. In this case, the author of the fixture can't
382+ # write such a finalizer because they have no way to capture the Context
383+ # in which the setup function was run, so we need to do it for them.
384+ reset_contextvars = _apply_contextvar_changes (context )
385+ if reset_contextvars is not None :
386+ request .addfinalizer (reset_contextvars )
387+
388+ return result
411389
412390 fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
413391
@@ -432,6 +410,57 @@ def _get_event_loop_fixture_id_for_async_fixture(
432410 return event_loop_fixture_id
433411
434412
413+ def _create_task_in_context (loop , coro , context ):
414+ """
415+ Return an asyncio task that runs the coro in the specified context,
416+ if possible.
417+
418+ This allows fixture setup and teardown to be run as separate asyncio tasks,
419+ while still being able to use context-manager idioms to maintain context
420+ variables and make those variables visible to test functions.
421+
422+ This is only fully supported on Python 3.11 and newer, as it requires
423+ the API added for https://github.com/python/cpython/issues/91150.
424+ On earlier versions, the returned task will use the default context instead.
425+ """
426+ try :
427+ return loop .create_task (coro , context = context )
428+ except TypeError :
429+ return loop .create_task (coro )
430+
431+
432+ def _apply_contextvar_changes (
433+ context : contextvars .Context ,
434+ ) -> Callable [[], None ] | None :
435+ """
436+ Copy contextvar changes from the given context to the current context.
437+
438+ If any contextvars were modified by the fixture, return a finalizer that
439+ will restore them.
440+ """
441+ context_tokens = []
442+ for var in context :
443+ try :
444+ if var .get () is context .get (var ):
445+ # This variable is not modified, so leave it as-is.
446+ continue
447+ except LookupError :
448+ # This variable isn't yet set in the current context at all.
449+ pass
450+ token = var .set (context .get (var ))
451+ context_tokens .append ((var , token ))
452+
453+ if not context_tokens :
454+ return None
455+
456+ def restore_contextvars ():
457+ while context_tokens :
458+ (var , token ) = context_tokens .pop ()
459+ var .reset (token )
460+
461+ return restore_contextvars
462+
463+
435464class PytestAsyncioFunction (Function ):
436465 """Base class for all test functions managed by pytest-asyncio."""
437466
0 commit comments