3333
3434import pluggy
3535import pytest
36+ from _pytest .scope import Scope
3637from pytest import (
3738 Class ,
3839 Collector ,
@@ -657,10 +658,6 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
657658 Session : "session" ,
658659}
659660
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-
664661
665662@pytest .hookimpl
666663def pytest_collectstart (collector : pytest .Collector ) -> None :
@@ -672,76 +669,9 @@ def pytest_collectstart(collector: pytest.Collector) -> None:
672669 )
673670 except StopIteration :
674671 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"
692673 collector .stash [_event_loop_fixture_id ] = event_loop_fixture_id
693674
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-
745675
746676@contextlib .contextmanager
747677def _temporary_event_loop_policy (policy : AbstractEventLoopPolicy ) -> Iterator [None ]:
@@ -971,21 +901,30 @@ def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector:
971901 raise pytest .UsageError (error_message )
972902
973903
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+ )
989928
990929
991930@contextlib .contextmanager
@@ -1004,16 +943,6 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]:
1004943 loop .close ()
1005944
1006945
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-
1017946@pytest .fixture (scope = "session" , autouse = True )
1018947def event_loop_policy () -> AbstractEventLoopPolicy :
1019948 """Return an instance of the policy used to create asyncio event loops."""
0 commit comments