10
10
import inspect
11
11
import socket
12
12
import sys
13
+ import traceback
13
14
import warnings
14
15
from asyncio import AbstractEventLoop , AbstractEventLoopPolicy
15
16
from collections .abc import (
54
55
else :
55
56
from typing_extensions import Concatenate , ParamSpec
56
57
58
+ if sys .version_info >= (3 , 11 ):
59
+ from asyncio import Runner
60
+ else :
61
+ from backports .asyncio .runner import Runner
57
62
58
63
_ScopeName = Literal ["session" , "package" , "module" , "class" , "function" ]
59
64
_T = TypeVar ("_T" )
@@ -230,14 +235,12 @@ def pytest_report_header(config: Config) -> list[str]:
230
235
]
231
236
232
237
233
- def _fixture_synchronizer (
234
- fixturedef : FixtureDef , event_loop : AbstractEventLoop
235
- ) -> Callable :
238
+ def _fixture_synchronizer (fixturedef : FixtureDef , runner : Runner ) -> Callable :
236
239
"""Returns a synchronous function evaluating the specified fixture."""
237
240
if inspect .isasyncgenfunction (fixturedef .func ):
238
- return _wrap_asyncgen_fixture (fixturedef .func , event_loop )
241
+ return _wrap_asyncgen_fixture (fixturedef .func , runner )
239
242
elif inspect .iscoroutinefunction (fixturedef .func ):
240
- return _wrap_async_fixture (fixturedef .func , event_loop )
243
+ return _wrap_async_fixture (fixturedef .func , runner )
241
244
else :
242
245
return fixturedef .func
243
246
@@ -278,7 +281,7 @@ def _wrap_asyncgen_fixture(
278
281
fixture_function : Callable [
279
282
AsyncGenFixtureParams , AsyncGeneratorType [AsyncGenFixtureYieldType , Any ]
280
283
],
281
- event_loop : AbstractEventLoop ,
284
+ runner : Runner ,
282
285
) -> Callable [
283
286
Concatenate [FixtureRequest , AsyncGenFixtureParams ], AsyncGenFixtureYieldType
284
287
]:
@@ -296,8 +299,7 @@ async def setup():
296
299
return res
297
300
298
301
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 )
301
303
302
304
reset_contextvars = _apply_contextvar_changes (context )
303
305
@@ -314,8 +316,7 @@ async def async_finalizer() -> None:
314
316
msg += "Yield only once."
315
317
raise ValueError (msg )
316
318
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 )
319
320
if reset_contextvars is not None :
320
321
reset_contextvars ()
321
322
@@ -333,7 +334,7 @@ def _wrap_async_fixture(
333
334
fixture_function : Callable [
334
335
AsyncFixtureParams , CoroutineType [Any , Any , AsyncFixtureReturnType ]
335
336
],
336
- event_loop : AbstractEventLoop ,
337
+ runner : Runner ,
337
338
) -> Callable [Concatenate [FixtureRequest , AsyncFixtureParams ], AsyncFixtureReturnType ]:
338
339
339
340
@functools .wraps (fixture_function ) # type: ignore[arg-type]
@@ -349,8 +350,7 @@ async def setup():
349
350
return res
350
351
351
352
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 )
354
354
355
355
# Copy the context vars modified by the setup task into the current
356
356
# context, and (if needed) add a finalizer to reset them.
@@ -610,22 +610,22 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
610
610
return
611
611
default_loop_scope = _get_default_test_loop_scope (metafunc .config )
612
612
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 "
614
614
# This specific fixture name may already be in metafunc.argnames, if this
615
615
# test indirectly depends on the fixture. For example, this is the case
616
616
# when the test depends on an async fixture, both of which share the same
617
617
# event loop fixture mark.
618
- if event_loop_fixture_id in metafunc .fixturenames :
618
+ if runner_fixture_id in metafunc .fixturenames :
619
619
return
620
620
fixturemanager = metafunc .config .pluginmanager .get_plugin ("funcmanage" )
621
621
assert fixturemanager is not None
622
622
# Add the scoped event loop fixture to Metafunc's list of fixture names and
623
623
# fixturedefs and leave the actual parametrization to pytest
624
624
# The fixture needs to be appended to avoid messing up the fixture evaluation
625
625
# 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
629
629
]
630
630
631
631
@@ -747,10 +747,10 @@ def pytest_runtest_setup(item: pytest.Item) -> None:
747
747
return
748
748
default_loop_scope = _get_default_test_loop_scope (item .config )
749
749
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 "
751
751
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 )
754
754
obj = getattr (item , "obj" , None )
755
755
if not getattr (obj , "hypothesis" , False ) and getattr (
756
756
obj , "is_hypothesis_test" , False
@@ -777,9 +777,9 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
777
777
or default_loop_scope
778
778
or fixturedef .scope
779
779
)
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 )
783
783
_make_asyncio_fixture_function (synchronizer , loop_scope )
784
784
with MonkeyPatch .context () as c :
785
785
if "request" not in fixturedef .argnames :
@@ -825,28 +825,51 @@ def _get_default_test_loop_scope(config: Config) -> _ScopeName:
825
825
return config .getini ("asyncio_default_test_loop_scope" )
826
826
827
827
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 :
829
840
@pytest .fixture (
830
841
scope = scope ,
831
- name = f"_{ scope } _event_loop " ,
842
+ name = f"_{ scope } _scoped_runner " ,
832
843
)
833
- def _scoped_event_loop (
844
+ def _scoped_runner (
834
845
* args , # Function needs to accept "cls" when collected by pytest.Class
835
846
event_loop_policy ,
836
- ) -> Iterator [asyncio . AbstractEventLoop ]:
847
+ ) -> Iterator [Runner ]:
837
848
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
+ )
844
867
845
- return _scoped_event_loop
868
+ return _scoped_runner
846
869
847
870
848
871
for 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 (
850
873
scope .value
851
874
)
852
875
0 commit comments