33
33
34
34
import pluggy
35
35
import pytest
36
+ from _pytest .scope import Scope
36
37
from pytest import (
37
38
Class ,
38
39
Collector ,
@@ -657,10 +658,6 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
657
658
Session : "session" ,
658
659
}
659
660
660
- # A stack used to push package-scoped loops during collection of a package
661
- # and pop those loops during collection of a Module
662
- __package_loop_stack : list [Callable [..., Any ]] = []
663
-
664
661
665
662
@pytest .hookimpl
666
663
def pytest_collectstart (collector : pytest .Collector ) -> None :
@@ -672,76 +669,9 @@ def pytest_collectstart(collector: pytest.Collector) -> None:
672
669
)
673
670
except StopIteration :
674
671
return
675
- # Session is not a PyCollector type, so it doesn't have a corresponding
676
- # "obj" attribute to attach a dynamic fixture function to.
677
- # However, there's only one session per pytest run, so there's no need to
678
- # create the fixture dynamically. We can simply define a session-scoped
679
- # event loop fixture once in the plugin code.
680
- if collector_scope == "session" :
681
- event_loop_fixture_id = _session_event_loop .__name__
682
- collector .stash [_event_loop_fixture_id ] = event_loop_fixture_id
683
- return
684
- # There seem to be issues when a fixture is shadowed by another fixture
685
- # and both differ in their params.
686
- # https://github.com/pytest-dev/pytest/issues/2043
687
- # https://github.com/pytest-dev/pytest/issues/11350
688
- # As such, we assign a unique name for each event_loop fixture.
689
- # The fixture name is stored in the collector's Stash, so it can
690
- # be injected when setting up the test
691
- event_loop_fixture_id = f"{ collector .nodeid } ::<event_loop>"
672
+ event_loop_fixture_id = f"_{ collector_scope } _event_loop"
692
673
collector .stash [_event_loop_fixture_id ] = event_loop_fixture_id
693
674
694
- @pytest .fixture (
695
- scope = collector_scope ,
696
- name = event_loop_fixture_id ,
697
- )
698
- def scoped_event_loop (
699
- * args , # Function needs to accept "cls" when collected by pytest.Class
700
- event_loop_policy ,
701
- ) -> Iterator [asyncio .AbstractEventLoop ]:
702
- new_loop_policy = event_loop_policy
703
- with (
704
- _temporary_event_loop_policy (new_loop_policy ),
705
- _provide_event_loop () as loop ,
706
- ):
707
- asyncio .set_event_loop (loop )
708
- yield loop
709
-
710
- # @pytest.fixture does not register the fixture anywhere, so pytest doesn't
711
- # know it exists. We work around this by attaching the fixture function to the
712
- # collected Python object, where it will be picked up by pytest.Class.collect()
713
- # or pytest.Module.collect(), respectively
714
- if type (collector ) is Package :
715
- # Packages do not have a corresponding Python object. Therefore, the fixture
716
- # for the package-scoped event loop is added to a stack. When a module inside
717
- # the package is collected, the module will attach the fixture to its
718
- # Python object.
719
- __package_loop_stack .append (scoped_event_loop )
720
- elif isinstance (collector , Module ):
721
- # Accessing Module.obj triggers a module import executing module-level
722
- # statements. A module-level pytest.skip statement raises the "Skipped"
723
- # OutcomeException or a Collector.CollectError, if the "allow_module_level"
724
- # kwargs is missing. These cases are handled correctly when they happen inside
725
- # Collector.collect(), but this hook runs before the actual collect call.
726
- # Therefore, we monkey patch Module.collect to add the scoped fixture to the
727
- # module before it runs the actual collection.
728
- def _patched_collect ():
729
- # If the collected module is a DoctestTextfile, collector.obj is None
730
- module = collector .obj
731
- if module is not None :
732
- module .__pytest_asyncio_scoped_event_loop = scoped_event_loop
733
- try :
734
- package_loop = __package_loop_stack .pop ()
735
- module .__pytest_asyncio_package_scoped_event_loop = package_loop
736
- except IndexError :
737
- pass
738
- return collector .__original_collect ()
739
-
740
- collector .__original_collect = collector .collect # type: ignore[attr-defined]
741
- collector .collect = _patched_collect # type: ignore[method-assign]
742
- elif isinstance (collector , Class ):
743
- collector .obj .__pytest_asyncio_scoped_event_loop = scoped_event_loop
744
-
745
675
746
676
@contextlib .contextmanager
747
677
def _temporary_event_loop_policy (policy : AbstractEventLoopPolicy ) -> Iterator [None ]:
@@ -971,21 +901,30 @@ def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
971
901
raise pytest .UsageError (error_message )
972
902
973
903
974
- @pytest .fixture (
975
- scope = "function" ,
976
- name = "_function_event_loop" ,
977
- )
978
- def _function_event_loop (
979
- * args , # Function needs to accept "cls" when collected by pytest.Class
980
- event_loop_policy ,
981
- ) -> Iterator [asyncio .AbstractEventLoop ]:
982
- new_loop_policy = event_loop_policy
983
- with (
984
- _temporary_event_loop_policy (new_loop_policy ),
985
- _provide_event_loop () as loop ,
986
- ):
987
- asyncio .set_event_loop (loop )
988
- yield loop
904
+ def _create_scoped_event_loop_fixture (scope : _ScopeName ) -> Callable :
905
+ @pytest .fixture (
906
+ scope = scope ,
907
+ name = f"_{ scope } _event_loop" ,
908
+ )
909
+ def _scoped_event_loop (
910
+ * args , # Function needs to accept "cls" when collected by pytest.Class
911
+ event_loop_policy ,
912
+ ) -> Iterator [asyncio .AbstractEventLoop ]:
913
+ new_loop_policy = event_loop_policy
914
+ with (
915
+ _temporary_event_loop_policy (new_loop_policy ),
916
+ _provide_event_loop () as loop ,
917
+ ):
918
+ asyncio .set_event_loop (loop )
919
+ yield loop
920
+
921
+ return _scoped_event_loop
922
+
923
+
924
+ for scope in Scope :
925
+ globals ()[f"_{ scope .value } _event_loop" ] = _create_scoped_event_loop_fixture (
926
+ scope .value
927
+ )
989
928
990
929
991
930
@contextlib .contextmanager
@@ -1004,16 +943,6 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
1004
943
loop .close ()
1005
944
1006
945
1007
- @pytest .fixture (scope = "session" )
1008
- def _session_event_loop (
1009
- request : FixtureRequest , event_loop_policy : AbstractEventLoopPolicy
1010
- ) -> Iterator [asyncio .AbstractEventLoop ]:
1011
- new_loop_policy = event_loop_policy
1012
- with _temporary_event_loop_policy (new_loop_policy ), _provide_event_loop () as loop :
1013
- asyncio .set_event_loop (loop )
1014
- yield loop
1015
-
1016
-
1017
946
@pytest .fixture (scope = "session" , autouse = True )
1018
947
def event_loop_policy () -> AbstractEventLoopPolicy :
1019
948
"""Return an instance of the policy used to create asyncio event loops."""
0 commit comments