@@ -319,12 +319,27 @@ def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
319
319
kwargs .pop (event_loop_fixture_id , None )
320
320
gen_obj = func (** _add_kwargs (func , kwargs , event_loop , request ))
321
321
322
- context = _event_loop_context .get (None )
323
-
324
322
async def setup ():
325
323
res = await gen_obj .__anext__ () # type: ignore[union-attr]
326
324
return res
327
325
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
+
328
343
def finalizer () -> None :
329
344
"""Yield again, to finalize."""
330
345
@@ -341,14 +356,39 @@ async def async_finalizer() -> None:
341
356
task = _create_task_in_context (event_loop , async_finalizer (), context )
342
357
event_loop .run_until_complete (task )
343
358
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
+
346
365
request .addfinalizer (finalizer )
347
366
return result
348
367
349
368
fixturedef .func = _asyncgen_fixture_wrapper # type: ignore[misc]
350
369
351
370
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
+
352
392
def _wrap_async_fixture (fixturedef : FixtureDef ) -> None :
353
393
fixture = fixturedef .func
354
394
@@ -365,10 +405,11 @@ async def setup():
365
405
res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
366
406
return res
367
407
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 ())
372
413
373
414
fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
374
415
@@ -592,46 +633,6 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
592
633
Session : "session" ,
593
634
}
594
635
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
-
635
636
# A stack used to push package-scoped loops during collection of a package
636
637
# and pop those loops during collection of a Module
637
638
__package_loop_stack : list [FixtureFunctionMarker | FixtureFunction ] = []
@@ -679,8 +680,7 @@ def scoped_event_loop(
679
680
loop = asyncio .new_event_loop ()
680
681
loop .__pytest_asyncio = True # type: ignore[attr-defined]
681
682
asyncio .set_event_loop (loop )
682
- with _set_event_loop_context ():
683
- yield loop
683
+ yield loop
684
684
loop .close ()
685
685
686
686
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
@@ -987,16 +987,9 @@ def wrap_in_sync(
987
987
988
988
@functools .wraps (func )
989
989
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
-
997
990
coro = func (* args , ** kwargs )
998
991
_loop = _get_event_loop_no_warn ()
999
- task = _create_task_in_context ( _loop , coro , context )
992
+ task = asyncio . ensure_future ( coro , loop = _loop )
1000
993
try :
1001
994
_loop .run_until_complete (task )
1002
995
except BaseException :
@@ -1105,8 +1098,7 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
1105
1098
# The magic value must be set as part of the function definition, because pytest
1106
1099
# seems to have multiple instances of the same FixtureDef or fixture function
1107
1100
loop .__original_fixture_loop = True # type: ignore[attr-defined]
1108
- with _set_event_loop_context ():
1109
- yield loop
1101
+ yield loop
1110
1102
loop .close ()
1111
1103
1112
1104
@@ -1119,8 +1111,7 @@ def _session_event_loop(
1119
1111
loop = asyncio .new_event_loop ()
1120
1112
loop .__pytest_asyncio = True # type: ignore[attr-defined]
1121
1113
asyncio .set_event_loop (loop )
1122
- with _set_event_loop_context ():
1123
- yield loop
1114
+ yield loop
1124
1115
loop .close ()
1125
1116
1126
1117
0 commit comments