34
34
Parser ,
35
35
PytestPluginManager ,
36
36
Session ,
37
+ StashKey ,
37
38
)
38
39
39
40
_R = TypeVar ("_R" )
55
56
SubRequest = Any
56
57
57
58
59
+ class PytestAsyncioError (Exception ):
60
+ """Base class for exceptions raised by pytest-asyncio"""
61
+
62
+
63
+ class MultipleEventLoopsRequestedError (PytestAsyncioError ):
64
+ """Raised when a test requests multiple asyncio event loops."""
65
+
66
+
58
67
class Mode (str , enum .Enum ):
59
68
AUTO = "auto"
60
69
STRICT = "strict"
@@ -345,6 +354,9 @@ def pytest_pycollect_makeitem(
345
354
return None
346
355
347
356
357
+ _event_loop_fixture_id = StashKey [str ]
358
+
359
+
348
360
@pytest .hookimpl
349
361
def pytest_collectstart (collector : pytest .Collector ):
350
362
if not isinstance (collector , (pytest .Class , pytest .Module )):
@@ -356,9 +368,19 @@ def pytest_collectstart(collector: pytest.Collector):
356
368
if not mark .name == "asyncio_event_loop" :
357
369
continue
358
370
371
+ # There seem to be issues when a fixture is shadowed by another fixture
372
+ # and both differ in their params.
373
+ # https://github.com/pytest-dev/pytest/issues/2043
374
+ # https://github.com/pytest-dev/pytest/issues/11350
375
+ # As such, we assign a unique name for each event_loop fixture.
376
+ # The fixture name is stored in the collector's Stash, so it can
377
+ # be injected when setting up the test
378
+ event_loop_fixture_id = f"{ collector .nodeid } ::<event_loop>"
379
+ collector .stash [_event_loop_fixture_id ] = event_loop_fixture_id
380
+
359
381
@pytest .fixture (
360
382
scope = "class" if isinstance (collector , pytest .Class ) else "module" ,
361
- name = "event_loop" ,
383
+ name = event_loop_fixture_id ,
362
384
)
363
385
def scoped_event_loop (
364
386
* args , # Function needs to accept "cls" when collected by pytest.Class
@@ -569,15 +591,38 @@ def inner(*args, **kwargs):
569
591
return inner
570
592
571
593
594
+ _MULTIPLE_LOOPS_REQUESTED_ERROR = dedent (
595
+ """\
596
+ Multiple asyncio event loops with different scopes have been requested
597
+ by %s. The test explicitly requests the event_loop fixture, while another
598
+ event loop is provided by %s.
599
+ Remove "event_loop" from the requested fixture in your test to run the test
600
+ in a larger-scoped event loop or remove the "asyncio_event_loop" mark to run
601
+ the test in a function-scoped event loop.
602
+ """
603
+ )
604
+
605
+
572
606
def pytest_runtest_setup (item : pytest .Item ) -> None :
573
607
marker = item .get_closest_marker ("asyncio" )
574
608
if marker is None :
575
609
return
610
+ event_loop_fixture_id = "event_loop"
611
+ for node , mark in item .iter_markers_with_node ("asyncio_event_loop" ):
612
+ scoped_event_loop_provider_node = node
613
+ event_loop_fixture_id = node .stash .get (_event_loop_fixture_id , None )
614
+ if event_loop_fixture_id :
615
+ break
576
616
fixturenames = item .fixturenames # type: ignore[attr-defined]
577
617
# inject an event loop fixture for all async tests
578
618
if "event_loop" in fixturenames :
619
+ if event_loop_fixture_id != "event_loop" :
620
+ raise MultipleEventLoopsRequestedError (
621
+ _MULTIPLE_LOOPS_REQUESTED_ERROR
622
+ % (item .nodeid , scoped_event_loop_provider_node .nodeid ),
623
+ )
579
624
fixturenames .remove ("event_loop" )
580
- fixturenames .insert (0 , "event_loop" )
625
+ fixturenames .insert (0 , event_loop_fixture_id )
581
626
obj = getattr (item , "obj" , None )
582
627
if not getattr (obj , "hypothesis" , False ) and getattr (
583
628
obj , "is_hypothesis_test" , False
0 commit comments