4
4
5
5
import asyncio
6
6
import contextlib
7
+ import contextvars
7
8
import enum
8
9
import functools
9
10
import inspect
@@ -318,6 +319,8 @@ def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
318
319
kwargs .pop (event_loop_fixture_id , None )
319
320
gen_obj = func (** _add_kwargs (func , kwargs , event_loop , request ))
320
321
322
+ context = _event_loop_context .get (None )
323
+
321
324
async def setup ():
322
325
res = await gen_obj .__anext__ () # type: ignore[union-attr]
323
326
return res
@@ -335,9 +338,11 @@ async def async_finalizer() -> None:
335
338
msg += "Yield only once."
336
339
raise ValueError (msg )
337
340
338
- event_loop .run_until_complete (async_finalizer ())
341
+ task = _create_task_in_context (event_loop , async_finalizer (), context )
342
+ event_loop .run_until_complete (task )
339
343
340
- result = event_loop .run_until_complete (setup ())
344
+ setup_task = _create_task_in_context (event_loop , setup (), context )
345
+ result = event_loop .run_until_complete (setup_task )
341
346
request .addfinalizer (finalizer )
342
347
return result
343
348
@@ -360,7 +365,10 @@ async def setup():
360
365
res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
361
366
return res
362
367
363
- return event_loop .run_until_complete (setup ())
368
+ task = _create_task_in_context (
369
+ event_loop , setup (), _event_loop_context .get (None )
370
+ )
371
+ return event_loop .run_until_complete (task )
364
372
365
373
fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
366
374
@@ -584,6 +592,46 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
584
592
Session : "session" ,
585
593
}
586
594
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
+
587
635
# A stack used to push package-scoped loops during collection of a package
588
636
# and pop those loops during collection of a Module
589
637
__package_loop_stack : list [FixtureFunctionMarker | FixtureFunction ] = []
@@ -631,7 +679,8 @@ def scoped_event_loop(
631
679
loop = asyncio .new_event_loop ()
632
680
loop .__pytest_asyncio = True # type: ignore[attr-defined]
633
681
asyncio .set_event_loop (loop )
634
- yield loop
682
+ with _set_event_loop_context ():
683
+ yield loop
635
684
loop .close ()
636
685
637
686
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
@@ -938,9 +987,16 @@ def wrap_in_sync(
938
987
939
988
@functools .wraps (func )
940
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
+
941
997
coro = func (* args , ** kwargs )
942
998
_loop = _get_event_loop_no_warn ()
943
- task = asyncio . ensure_future ( coro , loop = _loop )
999
+ task = _create_task_in_context ( _loop , coro , context )
944
1000
try :
945
1001
_loop .run_until_complete (task )
946
1002
except BaseException :
@@ -1049,7 +1105,8 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
1049
1105
# The magic value must be set as part of the function definition, because pytest
1050
1106
# seems to have multiple instances of the same FixtureDef or fixture function
1051
1107
loop .__original_fixture_loop = True # type: ignore[attr-defined]
1052
- yield loop
1108
+ with _set_event_loop_context ():
1109
+ yield loop
1053
1110
loop .close ()
1054
1111
1055
1112
@@ -1062,7 +1119,8 @@ def _session_event_loop(
1062
1119
loop = asyncio .new_event_loop ()
1063
1120
loop .__pytest_asyncio = True # type: ignore[attr-defined]
1064
1121
asyncio .set_event_loop (loop )
1065
- yield loop
1122
+ with _set_event_loop_context ():
1123
+ yield loop
1066
1124
loop .close ()
1067
1125
1068
1126
0 commit comments