1010import inspect
1111import socket
1212import sys
13+ import traceback
1314import warnings
1415from asyncio import AbstractEventLoop , AbstractEventLoopPolicy
1516from collections .abc import (
5455else :
5556 from typing_extensions import Concatenate , ParamSpec
5657
58+ if sys .version_info >= (3 , 11 ):
59+ from asyncio import Runner
60+ else :
61+ from backports .asyncio .runner import Runner
5762
5863_ScopeName = Literal ["session" , "package" , "module" , "class" , "function" ]
5964_T = TypeVar ("_T" )
@@ -230,14 +235,12 @@ def pytest_report_header(config: Config) -> list[str]:
230235 ]
231236
232237
233- def _fixture_synchronizer (
234- fixturedef : FixtureDef , event_loop : AbstractEventLoop
235- ) -> Callable :
238+ def _fixture_synchronizer (fixturedef : FixtureDef , runner : Runner ) -> Callable :
236239 """Returns a synchronous function evaluating the specified fixture."""
237240 if inspect .isasyncgenfunction (fixturedef .func ):
238- return _wrap_asyncgen_fixture (fixturedef .func , event_loop )
241+ return _wrap_asyncgen_fixture (fixturedef .func , runner )
239242 elif inspect .iscoroutinefunction (fixturedef .func ):
240- return _wrap_async_fixture (fixturedef .func , event_loop )
243+ return _wrap_async_fixture (fixturedef .func , runner )
241244 else :
242245 return fixturedef .func
243246
@@ -278,7 +281,7 @@ def _wrap_asyncgen_fixture(
278281 fixture_function : Callable [
279282 AsyncGenFixtureParams , AsyncGeneratorType [AsyncGenFixtureYieldType , Any ]
280283 ],
281- event_loop : AbstractEventLoop ,
284+ runner : Runner ,
282285) -> Callable [
283286 Concatenate [FixtureRequest , AsyncGenFixtureParams ], AsyncGenFixtureYieldType
284287]:
@@ -296,8 +299,7 @@ async def setup():
296299 return res
297300
298301 context = contextvars .copy_context ()
299- setup_task = _create_task_in_context (event_loop , setup (), context )
300- result = event_loop .run_until_complete (setup_task )
302+ result = runner .run (setup (), context = context )
301303
302304 reset_contextvars = _apply_contextvar_changes (context )
303305
@@ -314,8 +316,7 @@ async def async_finalizer() -> None:
314316 msg += "Yield only once."
315317 raise ValueError (msg )
316318
317- task = _create_task_in_context (event_loop , async_finalizer (), context )
318- event_loop .run_until_complete (task )
319+ runner .run (async_finalizer (), context = context )
319320 if reset_contextvars is not None :
320321 reset_contextvars ()
321322
@@ -333,7 +334,7 @@ def _wrap_async_fixture(
333334 fixture_function : Callable [
334335 AsyncFixtureParams , CoroutineType [Any , Any , AsyncFixtureReturnType ]
335336 ],
336- event_loop : AbstractEventLoop ,
337+ runner : Runner ,
337338) -> Callable [Concatenate [FixtureRequest , AsyncFixtureParams ], AsyncFixtureReturnType ]:
338339
339340 @functools .wraps (fixture_function ) # type: ignore[arg-type]
@@ -349,8 +350,7 @@ async def setup():
349350 return res
350351
351352 context = contextvars .copy_context ()
352- setup_task = _create_task_in_context (event_loop , setup (), context )
353- result = event_loop .run_until_complete (setup_task )
353+ result = runner .run (setup (), context = context )
354354
355355 # Copy the context vars modified by the setup task into the current
356356 # context, and (if needed) add a finalizer to reset them.
@@ -610,22 +610,22 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
610610 return
611611 default_loop_scope = _get_default_test_loop_scope (metafunc .config )
612612 loop_scope = _get_marked_loop_scope (marker , default_loop_scope )
613- event_loop_fixture_id = f"_{ loop_scope } _event_loop "
613+ runner_fixture_id = f"_{ loop_scope } _scoped_runner "
614614 # This specific fixture name may already be in metafunc.argnames, if this
615615 # test indirectly depends on the fixture. For example, this is the case
616616 # when the test depends on an async fixture, both of which share the same
617617 # event loop fixture mark.
618- if event_loop_fixture_id in metafunc .fixturenames :
618+ if runner_fixture_id in metafunc .fixturenames :
619619 return
620620 fixturemanager = metafunc .config .pluginmanager .get_plugin ("funcmanage" )
621621 assert fixturemanager is not None
622622 # Add the scoped event loop fixture to Metafunc's list of fixture names and
623623 # fixturedefs and leave the actual parametrization to pytest
624624 # The fixture needs to be appended to avoid messing up the fixture evaluation
625625 # order
626- metafunc .fixturenames .append (event_loop_fixture_id )
627- metafunc ._arg2fixturedefs [event_loop_fixture_id ] = fixturemanager ._arg2fixturedefs [
628- event_loop_fixture_id
626+ metafunc .fixturenames .append (runner_fixture_id )
627+ metafunc ._arg2fixturedefs [runner_fixture_id ] = fixturemanager ._arg2fixturedefs [
628+ runner_fixture_id
629629 ]
630630
631631
@@ -747,10 +747,10 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
747747 return
748748 default_loop_scope = _get_default_test_loop_scope (item .config )
749749 loop_scope = _get_marked_loop_scope (marker , default_loop_scope )
750- event_loop_fixture_id = f"_{ loop_scope } _event_loop "
750+ runner_fixture_id = f"_{ loop_scope } _scoped_runner "
751751 fixturenames = item .fixturenames # type: ignore[attr-defined]
752- if event_loop_fixture_id not in fixturenames :
753- fixturenames .append (event_loop_fixture_id )
752+ if runner_fixture_id not in fixturenames :
753+ fixturenames .append (runner_fixture_id )
754754 obj = getattr (item , "obj" , None )
755755 if not getattr (obj , "hypothesis" , False ) and getattr (
756756 obj , "is_hypothesis_test" , False
@@ -777,9 +777,9 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
777777 or default_loop_scope
778778 or fixturedef .scope
779779 )
780- event_loop_fixture_id = f"_{ loop_scope } _event_loop "
781- event_loop = request .getfixturevalue (event_loop_fixture_id )
782- synchronizer = _fixture_synchronizer (fixturedef , event_loop )
780+ runner_fixture_id = f"_{ loop_scope } _scoped_runner "
781+ runner = request .getfixturevalue (runner_fixture_id )
782+ synchronizer = _fixture_synchronizer (fixturedef , runner )
783783 _make_asyncio_fixture_function (synchronizer , loop_scope )
784784 with MonkeyPatch .context () as c :
785785 if "request" not in fixturedef .argnames :
@@ -825,28 +825,51 @@ def _get_default_test_loop_scope(config: Config) -> _ScopeName:
825825 return config .getini ("asyncio_default_test_loop_scope" )
826826
827827
828- def _create_scoped_event_loop_fixture (scope : _ScopeName ) -> Callable :
828+ _RUNNER_TEARDOWN_WARNING = """\
829+ An exception occurred during teardown of an asyncio.Runner. \
830+ The reason is likely that you closed the underlying event loop in a test, \
831+ which prevents the cleanup of asynchronous generators by the runner.
832+ This warning will become an error in future versions of pytest-asyncio. \
833+ Please ensure that your tests don't close the event loop. \
834+ Here is the traceback of the exception triggered during teardown:
835+ %s
836+ """
837+
838+
839+ def _create_scoped_runner_fixture (scope : _ScopeName ) -> Callable :
829840 @pytest .fixture (
830841 scope = scope ,
831- name = f"_{ scope } _event_loop " ,
842+ name = f"_{ scope } _scoped_runner " ,
832843 )
833- def _scoped_event_loop (
844+ def _scoped_runner (
834845 * args , # Function needs to accept "cls" when collected by pytest.Class
835846 event_loop_policy ,
836- ) -> Iterator [asyncio . AbstractEventLoop ]:
847+ ) -> Iterator [Runner ]:
837848 new_loop_policy = event_loop_policy
838- with (
839- _temporary_event_loop_policy (new_loop_policy ),
840- _provide_event_loop () as loop ,
841- ):
842- _set_event_loop (loop )
843- yield loop
849+ with _temporary_event_loop_policy (new_loop_policy ):
850+ runner = Runner ().__enter__ ()
851+ try :
852+ yield runner
853+ except Exception as e :
854+ runner .__exit__ (type (e ), e , e .__traceback__ )
855+ else :
856+ with warnings .catch_warnings ():
857+ warnings .filterwarnings (
858+ "ignore" , ".*BaseEventLoop.shutdown_asyncgens.*" , RuntimeWarning
859+ )
860+ try :
861+ runner .__exit__ (None , None , None )
862+ except RuntimeError :
863+ warnings .warn (
864+ _RUNNER_TEARDOWN_WARNING % traceback .format_exc (),
865+ RuntimeWarning ,
866+ )
844867
845- return _scoped_event_loop
868+ return _scoped_runner
846869
847870
848871for scope in Scope :
849- globals ()[f"_{ scope .value } _event_loop " ] = _create_scoped_event_loop_fixture (
872+ globals ()[f"_{ scope .value } _scoped_runner " ] = _create_scoped_runner_fixture (
850873 scope .value
851874 )
852875
0 commit comments