@@ -327,18 +327,7 @@ async def setup():
327
327
setup_task = _create_task_in_context (event_loop , setup (), context )
328
328
result = event_loop .run_until_complete (setup_task )
329
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 ))
330
+ reset_contextvars = _apply_contextvar_changes (context )
342
331
343
332
def finalizer () -> None :
344
333
"""Yield again, to finalize."""
@@ -355,38 +344,15 @@ async def async_finalizer() -> None:
355
344
356
345
task = _create_task_in_context (event_loop , async_finalizer (), context )
357
346
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 ()
364
349
365
350
request .addfinalizer (finalizer )
366
351
return result
367
352
368
353
fixturedef .func = _asyncgen_fixture_wrapper # type: ignore[misc]
369
354
370
355
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
-
390
356
def _wrap_async_fixture (fixturedef : FixtureDef ) -> None :
391
357
fixture = fixturedef .func
392
358
@@ -403,11 +369,23 @@ async def setup():
403
369
res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
404
370
return res
405
371
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
411
389
412
390
fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
413
391
@@ -432,6 +410,57 @@ def _get_event_loop_fixture_id_for_async_fixture(
432
410
return event_loop_fixture_id
433
411
434
412
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
+
435
464
class PytestAsyncioFunction (Function ):
436
465
"""Base class for all test functions managed by pytest-asyncio."""
437
466
0 commit comments