@@ -116,7 +116,7 @@ def fixture(
116
116
* ,
117
117
scope : _ScopeName | Callable [[str , Config ], _ScopeName ] = ...,
118
118
loop_scope : _ScopeName | None = ...,
119
- loop_factory : _ScopeName | Callable [[], AbstractEventLoop ] = ...,
119
+ loop_factory : Callable [[], AbstractEventLoop ] | None = ...,
120
120
params : Iterable [object ] | None = ...,
121
121
autouse : bool = ...,
122
122
ids : (
@@ -134,7 +134,7 @@ def fixture(
134
134
* ,
135
135
scope : _ScopeName | Callable [[str , Config ], _ScopeName ] = ...,
136
136
loop_scope : _ScopeName | None = ...,
137
- loop_factory : _ScopeName | Callable [[], AbstractEventLoop ] = ...,
137
+ loop_factory : Callable [[], AbstractEventLoop ] | None = ...,
138
138
params : Iterable [object ] | None = ...,
139
139
autouse : bool = ...,
140
140
ids : (
@@ -149,7 +149,7 @@ def fixture(
149
149
def fixture (
150
150
fixture_function : FixtureFunction [_P , _R ] | None = None ,
151
151
loop_scope : _ScopeName | None = None ,
152
- loop_factory : _ScopeName | Callable [[], AbstractEventLoop ] = ... ,
152
+ loop_factory : Callable [[], AbstractEventLoop ] | None = None ,
153
153
** kwargs : Any ,
154
154
) -> (
155
155
FixtureFunction [_P , _R ]
@@ -179,7 +179,9 @@ def _is_asyncio_fixture_function(obj: Any) -> bool:
179
179
180
180
181
181
def _make_asyncio_fixture_function (
182
- obj : Any , loop_scope : _ScopeName | None , loop_factory : _ScopeName | None
182
+ obj : Any ,
183
+ loop_scope : _ScopeName | None ,
184
+ loop_factory : Callable [[], AbstractEventLoop ] | None ,
183
185
) -> None :
184
186
if hasattr (obj , "__func__" ):
185
187
# instance method, check the function object
@@ -248,14 +250,13 @@ def _fixture_synchronizer(
248
250
fixturedef : FixtureDef ,
249
251
runner : Runner ,
250
252
request : FixtureRequest ,
251
- loop_factory : Callable [[], AbstractEventLoop ],
252
253
) -> Callable :
253
254
"""Returns a synchronous function evaluating the specified fixture."""
254
255
fixture_function = resolve_fixture_function (fixturedef , request )
255
256
if inspect .isasyncgenfunction (fixturedef .func ):
256
- return _wrap_asyncgen_fixture (fixture_function , runner , request , loop_factory ) # type: ignore[arg-type]
257
+ return _wrap_asyncgen_fixture (fixture_function , runner , request ) # type: ignore[arg-type]
257
258
elif inspect .iscoroutinefunction (fixturedef .func ):
258
- return _wrap_async_fixture (fixture_function , runner , request , loop_factory ) # type: ignore[arg-type]
259
+ return _wrap_async_fixture (fixture_function , runner , request ) # type: ignore[arg-type]
259
260
else :
260
261
return fixturedef .func
261
262
@@ -270,7 +271,6 @@ def _wrap_asyncgen_fixture(
270
271
],
271
272
runner : Runner ,
272
273
request : FixtureRequest ,
273
- loop_factory : Callable [[], AbstractEventLoop ],
274
274
) -> Callable [AsyncGenFixtureParams , AsyncGenFixtureYieldType ]:
275
275
@functools .wraps (fixture_function )
276
276
def _asyncgen_fixture_wrapper (
@@ -301,10 +301,6 @@ async def async_finalizer() -> None:
301
301
msg += "Yield only once."
302
302
raise ValueError (msg )
303
303
304
- if loop_factory :
305
- _loop = loop_factory ()
306
- asyncio .set_event_loop (_loop )
307
-
308
304
runner .run (async_finalizer (), context = context )
309
305
if reset_contextvars is not None :
310
306
reset_contextvars ()
@@ -325,9 +321,7 @@ def _wrap_async_fixture(
325
321
],
326
322
runner : Runner ,
327
323
request : FixtureRequest ,
328
- loop_factory : Callable [[], AbstractEventLoop ] | None = None ,
329
324
) -> Callable [AsyncFixtureParams , AsyncFixtureReturnType ]:
330
-
331
325
@functools .wraps (fixture_function ) # type: ignore[arg-type]
332
326
def _async_fixture_wrapper (
333
327
* args : AsyncFixtureParams .args ,
@@ -339,10 +333,6 @@ async def setup():
339
333
340
334
context = contextvars .copy_context ()
341
335
342
- # ensure loop_factory gets ran before we start running...
343
- if loop_factory :
344
- asyncio .set_event_loop (loop_factory ())
345
-
346
336
result = runner .run (setup (), context = context )
347
337
# Copy the context vars modified by the setup task into the current
348
338
# context, and (if needed) add a finalizer to reset them.
@@ -445,9 +435,7 @@ def _can_substitute(item: Function) -> bool:
445
435
446
436
def runtest (self ) -> None :
447
437
# print(self.obj.pytestmark[0].__dict__)
448
- synchronized_obj = wrap_in_sync (
449
- self .obj , self .obj .pytestmark [0 ].kwargs .get ("loop_factory" , None )
450
- )
438
+ synchronized_obj = wrap_in_sync (self .obj )
451
439
with MonkeyPatch .context () as c :
452
440
c .setattr (self , "obj" , synchronized_obj )
453
441
super ().runtest ()
@@ -559,16 +547,32 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass(
559
547
560
548
561
549
@contextlib .contextmanager
562
- def _temporary_event_loop_policy (policy : AbstractEventLoopPolicy ) -> Iterator [None ]:
550
+ def _temporary_event_loop_policy (
551
+ policy : AbstractEventLoopPolicy ,
552
+ loop_facotry : Callable [..., AbstractEventLoop ] | None ,
553
+ ) -> Iterator [None ]:
554
+
563
555
old_loop_policy = _get_event_loop_policy ()
564
556
try :
565
557
old_loop = _get_event_loop_no_warn ()
566
558
except RuntimeError :
567
559
old_loop = None
560
+ # XXX: For some reason this function can override runner's
561
+ # _loop_factory (At least observed on backported versions of Runner)
562
+ # so we need to re-override if existing...
563
+ if loop_facotry :
564
+ _loop = loop_facotry ()
565
+ _set_event_loop (_loop )
566
+ else :
567
+ _loop = None
568
+
568
569
_set_event_loop_policy (policy )
569
570
try :
570
571
yield
571
572
finally :
573
+ if _loop :
574
+ # Do not let BaseEventLoop.__del__ complain!
575
+ _loop .close ()
572
576
_set_event_loop_policy (old_loop_policy )
573
577
_set_event_loop (old_loop )
574
578
@@ -654,7 +658,6 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
654
658
655
659
def wrap_in_sync (
656
660
func : Callable [..., Awaitable [Any ]],
657
- loop_factory : Callable [[], AbstractEventLoop ] | None = None ,
658
661
):
659
662
"""
660
663
Return a sync wrapper around an async function executing it in the
@@ -663,26 +666,18 @@ def wrap_in_sync(
663
666
664
667
@functools .wraps (func )
665
668
def inner (* args , ** kwargs ):
666
- _last_loop = asyncio .get_event_loop ()
667
- if loop_factory :
668
- _loop = loop_factory ()
669
- asyncio .set_event_loop (_loop )
670
- else :
671
- _loop = asyncio .get_event_loop ()
669
+ _loop = asyncio .get_event_loop ()
672
670
task = asyncio .ensure_future (func (* args , ** kwargs ), loop = _loop )
673
671
try :
674
672
_loop .run_until_complete (task )
675
673
except BaseException :
676
-
677
674
# run_until_complete doesn't get the result from exceptions
678
675
# that are not subclasses of `Exception`. Consume all
679
676
# exceptions to prevent asyncio's warning from logging.
680
677
if task .done () and not task .cancelled ():
681
678
task .exception ()
682
679
raise
683
680
684
- asyncio .set_event_loop (_last_loop )
685
-
686
681
return inner
687
682
688
683
@@ -726,7 +721,7 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
726
721
727
722
runner_fixture_id = f"_{ loop_scope } _scoped_runner"
728
723
runner = request .getfixturevalue (runner_fixture_id )
729
- synchronizer = _fixture_synchronizer (fixturedef , runner , request , loop_factory )
724
+ synchronizer = _fixture_synchronizer (fixturedef , runner , request )
730
725
_make_asyncio_fixture_function (synchronizer , loop_scope , loop_factory )
731
726
with MonkeyPatch .context () as c :
732
727
c .setattr (fixturedef , "func" , synchronizer )
@@ -754,7 +749,8 @@ def _get_marked_loop_scope(
754
749
and set (asyncio_marker .kwargs ) - {"loop_scope" , "scope" , "loop_factory" }
755
750
):
756
751
raise ValueError (
757
- "mark.asyncio accepts only a keyword arguments 'loop_scope' or 'loop_factory'"
752
+ "mark.asyncio accepts only a keyword arguments 'loop_scope'"
753
+ " or 'loop_factory'"
758
754
)
759
755
if "scope" in asyncio_marker .kwargs :
760
756
if "loop_scope" in asyncio_marker .kwargs :
@@ -784,17 +780,32 @@ def _get_default_test_loop_scope(config: Config) -> _ScopeName:
784
780
"""
785
781
786
782
783
+ def _get_loop_facotry (
784
+ request : FixtureRequest ,
785
+ ) -> Callable [[], AbstractEventLoop ] | None :
786
+ if asyncio_mark := request ._pyfuncitem .get_closest_marker ("asyncio" ):
787
+ factory = asyncio_mark .kwargs .get ("loop_factory" , None )
788
+ print (f"FACTORY { factory } " )
789
+ return factory
790
+ else :
791
+ return request .obj .__dict__ .get ("_loop_factory" , None ) # type: ignore[attr-defined]
792
+
793
+
787
794
def _create_scoped_runner_fixture (scope : _ScopeName ) -> Callable :
788
795
@pytest .fixture (
789
796
scope = scope ,
790
797
name = f"_{ scope } _scoped_runner" ,
791
798
)
792
799
def _scoped_runner (
793
- event_loop_policy ,
800
+ event_loop_policy : AbstractEventLoopPolicy , request : FixtureRequest
794
801
) -> Iterator [Runner ]:
795
802
new_loop_policy = event_loop_policy
796
- with _temporary_event_loop_policy (new_loop_policy ):
797
- runner = Runner ().__enter__ ()
803
+
804
+ # We need to get the factory now because
805
+ # _temporary_event_loop_policy can override the Runner
806
+ factory = _get_loop_facotry (request )
807
+ with _temporary_event_loop_policy (new_loop_policy , factory ):
808
+ runner = Runner (loop_factory = factory ).__enter__ ()
798
809
try :
799
810
yield runner
800
811
except Exception as e :
0 commit comments