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
54
55
_ScopeName = Literal ["session" , "package" , "module" , "class" , "function" ]
55
56
_T = TypeVar ("_T" )
56
57
58
+
57
59
SimpleFixtureFunction = TypeVar (
58
60
"SimpleFixtureFunction" , bound = Callable [..., Awaitable [object ]]
59
61
)
@@ -318,6 +320,8 @@ def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
318
320
kwargs .pop (event_loop_fixture_id , None )
319
321
gen_obj = func (** _add_kwargs (func , kwargs , event_loop , request ))
320
322
323
+ context = _event_loop_context .get (None )
324
+
321
325
async def setup ():
322
326
res = await gen_obj .__anext__ () # type: ignore[union-attr]
323
327
return res
@@ -335,9 +339,11 @@ async def async_finalizer() -> None:
335
339
msg += "Yield only once."
336
340
raise ValueError (msg )
337
341
338
- event_loop .run_until_complete (async_finalizer ())
342
+ task = _create_task_in_context (event_loop , async_finalizer (), context )
343
+ event_loop .run_until_complete (task )
339
344
340
- result = event_loop .run_until_complete (setup ())
345
+ setup_task = _create_task_in_context (event_loop , setup (), context )
346
+ result = event_loop .run_until_complete (setup_task )
341
347
request .addfinalizer (finalizer )
342
348
return result
343
349
@@ -360,7 +366,10 @@ async def setup():
360
366
res = await func (** _add_kwargs (func , kwargs , event_loop , request ))
361
367
return res
362
368
363
- return event_loop .run_until_complete (setup ())
369
+ task = _create_task_in_context (
370
+ event_loop , setup (), _event_loop_context .get (None )
371
+ )
372
+ return event_loop .run_until_complete (task )
364
373
365
374
fixturedef .func = _async_fixture_wrapper # type: ignore[misc]
366
375
@@ -584,6 +593,46 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
584
593
Session : "session" ,
585
594
}
586
595
596
+ # _event_loop_context stores the Context in which asyncio tasks on the fixture
597
+ # event loop should be run. After fixture setup, individual async test functions
598
+ # are run on copies of this context.
599
+ _event_loop_context : contextvars .ContextVar [contextvars .Context ] = (
600
+ contextvars .ContextVar ("pytest_asyncio_event_loop_context" )
601
+ )
602
+
603
+
604
+ @contextlib .contextmanager
605
+ def _set_event_loop_context ():
606
+ """Set event_loop_context to a copy of the calling thread's current context."""
607
+ context = contextvars .copy_context ()
608
+ token = _event_loop_context .set (context )
609
+ try :
610
+ yield
611
+ finally :
612
+ _event_loop_context .reset (token )
613
+
614
+
615
+ def _create_task_in_context (loop , coro , context ):
616
+ """
617
+ Return an asyncio task that runs the coro in the specified context,
618
+ if possible.
619
+
620
+ This allows fixture setup and teardown to be run as separate asyncio tasks,
621
+ while still being able to use context-manager idioms to maintain context
622
+ variables and make those variables visible to test functions.
623
+
624
+ This is only fully supported on Python 3.11 and newer, as it requires
625
+ the API added for https://github.com/python/cpython/issues/91150.
626
+ On earlier versions, the returned task will use the default context instead.
627
+ """
628
+ if context is not None :
629
+ try :
630
+ return loop .create_task (coro , context = context )
631
+ except TypeError :
632
+ pass
633
+ return loop .create_task (coro )
634
+
635
+
587
636
# A stack used to push package-scoped loops during collection of a package
588
637
# and pop those loops during collection of a Module
589
638
__package_loop_stack : list [FixtureFunctionMarker | FixtureFunction ] = []
@@ -631,7 +680,8 @@ def scoped_event_loop(
631
680
loop = asyncio .new_event_loop ()
632
681
loop .__pytest_asyncio = True # type: ignore[attr-defined]
633
682
asyncio .set_event_loop (loop )
634
- yield loop
683
+ with _set_event_loop_context ():
684
+ yield loop
635
685
loop .close ()
636
686
637
687
# @pytest.fixture does not register the fixture anywhere, so pytest doesn't
@@ -938,9 +988,16 @@ def wrap_in_sync(
938
988
939
989
@functools .wraps (func )
940
990
def inner (* args , ** kwargs ):
991
+ # Give each test its own context based on the loop's main context.
992
+ context = _event_loop_context .get (None )
993
+ if context is not None :
994
+ # We are using our own event loop fixture, so make a new copy of the
995
+ # fixture context so that the test won't pollute it.
996
+ context = context .copy ()
997
+
941
998
coro = func (* args , ** kwargs )
942
999
_loop = _get_event_loop_no_warn ()
943
- task = asyncio . ensure_future ( coro , loop = _loop )
1000
+ task = _create_task_in_context ( _loop , coro , context )
944
1001
try :
945
1002
_loop .run_until_complete (task )
946
1003
except BaseException :
@@ -1049,7 +1106,8 @@ def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]:
1049
1106
# The magic value must be set as part of the function definition, because pytest
1050
1107
# seems to have multiple instances of the same FixtureDef or fixture function
1051
1108
loop .__original_fixture_loop = True # type: ignore[attr-defined]
1052
- yield loop
1109
+ with _set_event_loop_context ():
1110
+ yield loop
1053
1111
loop .close ()
1054
1112
1055
1113
@@ -1062,7 +1120,8 @@ def _session_event_loop(
1062
1120
loop = asyncio .new_event_loop ()
1063
1121
loop .__pytest_asyncio = True # type: ignore[attr-defined]
1064
1122
asyncio .set_event_loop (loop )
1065
- yield loop
1123
+ with _set_event_loop_context ():
1124
+ yield loop
1066
1125
loop .close ()
1067
1126
1068
1127
0 commit comments