From f3300d4047a92f09f59e008445d8cdddb27180a3 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Wed, 9 Jul 2025 10:34:34 -0500 Subject: [PATCH 01/12] Add 2 new functions for using eventloops in non-deprecated ways --- pytest_asyncio/plugin.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 29252b3e..2a9debdd 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -631,6 +631,21 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No _set_event_loop(old_loop) +@contextlib.contextmanager +def _temporary_event_loop(loop:AbstractEventLoop): + try: + old_event_loop = asyncio.get_event_loop() + except RuntimeError: + old_event_loop = None + + asyncio.set_event_loop(old_event_loop) + try: + yield + finally: + asyncio.set_event_loop(old_event_loop) + + + def _get_event_loop_policy() -> AbstractEventLoopPolicy: with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) @@ -826,6 +841,9 @@ def _scoped_runner( RuntimeWarning, ) + + + return _scoped_runner @@ -834,6 +852,11 @@ def _scoped_runner( scope.value ) +@pytest.fixture(scope="session", autouse=True) +def new_event_loop() -> AbstractEventLoop: + """Creates a new eventloop for different tests being ran""" + return asyncio.new_event_loop() + @pytest.fixture(scope="session", autouse=True) def event_loop_policy() -> AbstractEventLoopPolicy: From 2f2298818f681afccd3f0f6c157ffac39abc0724 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Wed, 9 Jul 2025 10:49:44 -0500 Subject: [PATCH 02/12] Add to timeline --- changelog.d/1164.added.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1164.added.rst diff --git a/changelog.d/1164.added.rst b/changelog.d/1164.added.rst new file mode 100644 index 00000000..321bacd4 --- /dev/null +++ b/changelog.d/1164.added.rst @@ -0,0 +1 @@ +Added ``new_event_loop`` fixture to provide an alternative approch to event loop policies being deprecated \ No newline at end of file From 7ea5e803e3636b47b4166d577b67c73c340840f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:45:44 +0000 Subject: [PATCH 03/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pytest_asyncio/plugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2a9debdd..bbe4b30e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -632,20 +632,19 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No @contextlib.contextmanager -def _temporary_event_loop(loop:AbstractEventLoop): +def _temporary_event_loop(loop: AbstractEventLoop): try: old_event_loop = asyncio.get_event_loop() except RuntimeError: old_event_loop = None - + asyncio.set_event_loop(old_event_loop) try: - yield + yield finally: asyncio.set_event_loop(old_event_loop) - def _get_event_loop_policy() -> AbstractEventLoopPolicy: with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) @@ -841,9 +840,6 @@ def _scoped_runner( RuntimeWarning, ) - - - return _scoped_runner @@ -852,6 +848,7 @@ def _scoped_runner( scope.value ) + @pytest.fixture(scope="session", autouse=True) def new_event_loop() -> AbstractEventLoop: """Creates a new eventloop for different tests being ran""" From 95d5930081e2db2f0167144debd94c17889121b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:53:25 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog.d/1164.added.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1164.added.rst b/changelog.d/1164.added.rst index 321bacd4..e4cf9e53 100644 --- a/changelog.d/1164.added.rst +++ b/changelog.d/1164.added.rst @@ -1 +1 @@ -Added ``new_event_loop`` fixture to provide an alternative approch to event loop policies being deprecated \ No newline at end of file +Added ``new_event_loop`` fixture to provide an alternative approch to event loop policies being deprecated From 5f7edb6e0f9f3bd868f573f59d0edeead99aab16 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Thu, 10 Jul 2025 16:48:51 -0500 Subject: [PATCH 05/12] Incomplete need to figure out how to get loop_factory / multiple into asyncio.Runner --- pytest_asyncio/plugin.py | 70 +++++++++++++++---------- tests/markers/test_invalid_arguments.py | 12 +++-- tests/test_asyncio_mark.py | 32 +++++++++++ 3 files changed, 82 insertions(+), 32 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index bbe4b30e..a462482e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -417,6 +417,8 @@ def restore_contextvars(): class PytestAsyncioFunction(Function): """Base class for all test functions managed by pytest-asyncio.""" + loop_factory: Callable[[], AbstractEventLoop] | None + @classmethod def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | None: """ @@ -431,7 +433,12 @@ def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | N return None @classmethod - def _from_function(cls, function: Function, /) -> Function: + def _from_function( + cls, + function: Function, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + /, + ) -> Function: """ Instantiates this specific PytestAsyncioFunction type from the specified Function item. @@ -447,6 +454,7 @@ def _from_function(cls, function: Function, /) -> Function: keywords=function.keywords, originalname=function.originalname, ) + subclass_instance.loop_factory = loop_factory subclass_instance.own_markers = function.own_markers assert subclass_instance.own_markers == function.own_markers return subclass_instance @@ -610,9 +618,27 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( node.config ) == Mode.AUTO and not node.get_closest_marker("asyncio"): node.add_marker("asyncio") - if node.get_closest_marker("asyncio"): - updated_item = specialized_item_class._from_function(node) - updated_node_collection.append(updated_item) + if asyncio_marker := node.get_closest_marker("asyncio"): + if loop_factory := asyncio_marker.kwargs.get("loop_factory", None): + # multiply if loop_factory is an iterable object of factories + if hasattr(loop_factory, "__iter__"): + updated_item = [ + specialized_item_class._from_function(node, lf) + for lf in loop_factory + ] + else: + updated_item = specialized_item_class._from_function( + node, loop_factory + ) + else: + updated_item = specialized_item_class._from_function(node) + + # we could have multiple factroies to test if so, + # multiply the number of functions for us... + if isinstance(updated_item, list): + updated_node_collection.extend(updated_item) + else: + updated_node_collection.append(updated_item) hook_result.force_result(updated_node_collection) @@ -631,20 +657,6 @@ def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[No _set_event_loop(old_loop) -@contextlib.contextmanager -def _temporary_event_loop(loop: AbstractEventLoop): - try: - old_event_loop = asyncio.get_event_loop() - except RuntimeError: - old_event_loop = None - - asyncio.set_event_loop(old_event_loop) - try: - yield - finally: - asyncio.set_event_loop(old_event_loop) - - def _get_event_loop_policy() -> AbstractEventLoopPolicy: with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) @@ -753,8 +765,15 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: or default_loop_scope or fixturedef.scope ) + # XXX: Currently Confused as to where to debug and harvest and get the runner to use the loop_factory argument. + loop_factory = getattr(fixturedef.func, "loop_factory", None) + + print(f"LOOP FACTORY: {loop_factory} {fixturedef.func}") + sys.stdout.flush() + runner_fixture_id = f"_{loop_scope}_scoped_runner" - runner = request.getfixturevalue(runner_fixture_id) + runner: Runner = request.getfixturevalue(runner_fixture_id) + synchronizer = _fixture_synchronizer(fixturedef, runner, request) _make_asyncio_fixture_function(synchronizer, loop_scope) with MonkeyPatch.context() as c: @@ -779,9 +798,12 @@ def _get_marked_loop_scope( ) -> _ScopeName: assert asyncio_marker.name == "asyncio" if asyncio_marker.args or ( - asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope"} + asyncio_marker.kwargs + and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factory"} ): - raise ValueError("mark.asyncio accepts only a keyword argument 'loop_scope'.") + raise ValueError( + "mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'." + ) if "scope" in asyncio_marker.kwargs: if "loop_scope" in asyncio_marker.kwargs: raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) @@ -849,12 +871,6 @@ def _scoped_runner( ) -@pytest.fixture(scope="session", autouse=True) -def new_event_loop() -> AbstractEventLoop: - """Creates a new eventloop for different tests being ran""" - return asyncio.new_event_loop() - - @pytest.fixture(scope="session", autouse=True) def event_loop_policy() -> AbstractEventLoopPolicy: """Return an instance of the policy used to create asyncio event loops.""" diff --git a/tests/markers/test_invalid_arguments.py b/tests/markers/test_invalid_arguments.py index 2d5c3552..a7e499a3 100644 --- a/tests/markers/test_invalid_arguments.py +++ b/tests/markers/test_invalid_arguments.py @@ -40,9 +40,7 @@ async def test_anything(): ) result = pytester.runpytest_subprocess() result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only a keyword argument*"] - ) + result.stdout.fnmatch_lines([""]) def test_error_when_wrong_keyword_argument_is_passed( @@ -62,7 +60,9 @@ async def test_anything(): result = pytester.runpytest_subprocess() result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only a keyword argument 'loop_scope'*"] + [ + "*ValueError: mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'*" + ] ) @@ -83,5 +83,7 @@ async def test_anything(): result = pytester.runpytest_subprocess() result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( - ["*ValueError: mark.asyncio accepts only a keyword argument*"] + [ + "*ValueError: mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'*" + ] ) diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 81731adb..094093c3 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -223,3 +223,35 @@ async def test_a(session_loop_fixture): result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(passed=1) + + +def test_asyncio_marker_event_loop_factories(pytester: Pytester): + pytester.makeini( + dedent( + """\ + [pytest] + asyncio_default_fixture_loop_scope = function + asyncio_default_test_loop_scope = module + """ + ) + ) + + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest_asyncio + import pytest + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest.mark.asyncio(loop_factory=CustomEventLoop) + async def test_has_different_event_loop(): + assert type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" + """ + ) + ) + + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) From aa95acd6b6870a9763e2e5ac1e11857c8a1cba7e Mon Sep 17 00:00:00 2001 From: Vizonex Date: Fri, 25 Jul 2025 19:53:05 -0500 Subject: [PATCH 06/12] figured out loop_factory :) --- pytest_asyncio/plugin.py | 92 ++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index a462482e..ed8f8194 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -49,6 +49,7 @@ PytestPluginManager, ) +from typing import Callable if sys.version_info >= (3, 10): from typing import ParamSpec else: @@ -134,6 +135,7 @@ def fixture( *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., loop_scope: _ScopeName | None = ..., + loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: ( @@ -151,6 +153,7 @@ def fixture( *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., loop_scope: _ScopeName | None = ..., + loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: ( @@ -165,20 +168,21 @@ def fixture( def fixture( fixture_function: FixtureFunction[_P, _R] | None = None, loop_scope: _ScopeName | None = None, + loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., **kwargs: Any, ) -> ( FixtureFunction[_P, _R] | Callable[[FixtureFunction[_P, _R]], FixtureFunction[_P, _R]] ): if fixture_function is not None: - _make_asyncio_fixture_function(fixture_function, loop_scope) + _make_asyncio_fixture_function(fixture_function, loop_scope, loop_factory) return pytest.fixture(fixture_function, **kwargs) else: @functools.wraps(fixture) def inner(fixture_function: FixtureFunction[_P, _R]) -> FixtureFunction[_P, _R]: - return fixture(fixture_function, loop_scope=loop_scope, **kwargs) + return fixture(fixture_function, loop_factory=loop_factory, loop_scope=loop_scope, **kwargs) return inner @@ -188,12 +192,13 @@ def _is_asyncio_fixture_function(obj: Any) -> bool: return getattr(obj, "_force_asyncio_fixture", False) -def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None) -> None: +def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None, loop_factory: _ScopeName | None) -> None: if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ obj._force_asyncio_fixture = True obj._loop_scope = loop_scope + obj._loop_factory = loop_factory def _is_coroutine_or_asyncgen(obj: Any) -> bool: @@ -280,14 +285,14 @@ def pytest_report_header(config: Config) -> list[str]: def _fixture_synchronizer( - fixturedef: FixtureDef, runner: Runner, request: FixtureRequest + fixturedef: FixtureDef, runner: Runner, request: FixtureRequest, loop_factory: Callable[[], AbstractEventLoop] ) -> Callable: """Returns a synchronous function evaluating the specified fixture.""" fixture_function = resolve_fixture_function(fixturedef, request) if inspect.isasyncgenfunction(fixturedef.func): - return _wrap_asyncgen_fixture(fixture_function, runner, request) # type: ignore[arg-type] + return _wrap_asyncgen_fixture(fixture_function, runner, request, loop_factory) # type: ignore[arg-type] elif inspect.iscoroutinefunction(fixturedef.func): - return _wrap_async_fixture(fixture_function, runner, request) # type: ignore[arg-type] + return _wrap_async_fixture(fixture_function, runner, request, loop_factory) # type: ignore[arg-type] else: return fixturedef.func @@ -302,6 +307,7 @@ def _wrap_asyncgen_fixture( ], runner: Runner, request: FixtureRequest, + loop_factory:Callable[[], AbstractEventLoop] ) -> Callable[AsyncGenFixtureParams, AsyncGenFixtureYieldType]: @functools.wraps(fixture_function) def _asyncgen_fixture_wrapper( @@ -331,6 +337,9 @@ async def async_finalizer() -> None: msg = "Async generator fixture didn't stop." msg += "Yield only once." raise ValueError(msg) + if loop_factory: + _loop = loop_factory() + asyncio.set_event_loop(_loop) runner.run(async_finalizer(), context=context) if reset_contextvars is not None: @@ -352,6 +361,7 @@ def _wrap_async_fixture( ], runner: Runner, request: FixtureRequest, + loop_factory: Callable[[], AbstractEventLoop] | None = None ) -> Callable[AsyncFixtureParams, AsyncFixtureReturnType]: @functools.wraps(fixture_function) def _async_fixture_wrapper( @@ -363,8 +373,12 @@ async def setup(): return res context = contextvars.copy_context() - result = runner.run(setup(), context=context) + # ensure loop_factory gets ran before we start running... + if loop_factory: + asyncio.set_event_loop(loop_factory()) + + result = runner.run(setup(), context=context) # Copy the context vars modified by the setup task into the current # context, and (if needed) add a finalizer to reset them. # @@ -417,8 +431,6 @@ def restore_contextvars(): class PytestAsyncioFunction(Function): """Base class for all test functions managed by pytest-asyncio.""" - loop_factory: Callable[[], AbstractEventLoop] | None - @classmethod def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | None: """ @@ -433,12 +445,7 @@ def item_subclass_for(cls, item: Function, /) -> type[PytestAsyncioFunction] | N return None @classmethod - def _from_function( - cls, - function: Function, - loop_factory: Callable[[], AbstractEventLoop] | None = None, - /, - ) -> Function: + def _from_function(cls, function: Function, /) -> Function: """ Instantiates this specific PytestAsyncioFunction type from the specified Function item. @@ -454,7 +461,6 @@ def _from_function( keywords=function.keywords, originalname=function.originalname, ) - subclass_instance.loop_factory = loop_factory subclass_instance.own_markers = function.own_markers assert subclass_instance.own_markers == function.own_markers return subclass_instance @@ -516,6 +522,16 @@ def _can_substitute(item: Function) -> bool: func = item.obj return inspect.iscoroutinefunction(func) +<<<<<<< HEAD +======= + def runtest(self) -> None: + # print(self.obj.pytestmark[0].__dict__) + synchronized_obj = wrap_in_sync(self.obj, self.obj.pytestmark[0].kwargs.get('loop_factory', None)) + with MonkeyPatch.context() as c: + c.setattr(self, "obj", synchronized_obj) + super().runtest() + +>>>>>>> edfbfef (figured out loop_factory :)) class AsyncGenerator(PytestAsyncioFunction): """Pytest item created by an asynchronous generator""" @@ -618,27 +634,9 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( node.config ) == Mode.AUTO and not node.get_closest_marker("asyncio"): node.add_marker("asyncio") - if asyncio_marker := node.get_closest_marker("asyncio"): - if loop_factory := asyncio_marker.kwargs.get("loop_factory", None): - # multiply if loop_factory is an iterable object of factories - if hasattr(loop_factory, "__iter__"): - updated_item = [ - specialized_item_class._from_function(node, lf) - for lf in loop_factory - ] - else: - updated_item = specialized_item_class._from_function( - node, loop_factory - ) - else: - updated_item = specialized_item_class._from_function(node) - - # we could have multiple factroies to test if so, - # multiply the number of functions for us... - if isinstance(updated_item, list): - updated_node_collection.extend(updated_item) - else: - updated_node_collection.append(updated_item) + if node.get_closest_marker("asyncio"): + updated_item = specialized_item_class._from_function(node) + updated_node_collection.append(updated_item) hook_result.force_result(updated_node_collection) @@ -740,12 +738,12 @@ def _synchronize_coroutine( Return a sync wrapper around a coroutine executing it in the specified runner and context. """ - @functools.wraps(func) def inner(*args, **kwargs): coro = func(*args, **kwargs) runner.run(coro, context=context) + asyncio.set_event_loop(_last_loop) return inner @@ -765,17 +763,12 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: or default_loop_scope or fixturedef.scope ) - # XXX: Currently Confused as to where to debug and harvest and get the runner to use the loop_factory argument. loop_factory = getattr(fixturedef.func, "loop_factory", None) - print(f"LOOP FACTORY: {loop_factory} {fixturedef.func}") - sys.stdout.flush() - runner_fixture_id = f"_{loop_scope}_scoped_runner" - runner: Runner = request.getfixturevalue(runner_fixture_id) - - synchronizer = _fixture_synchronizer(fixturedef, runner, request) - _make_asyncio_fixture_function(synchronizer, loop_scope) + runner = request.getfixturevalue(runner_fixture_id) + synchronizer = _fixture_synchronizer(fixturedef, runner, request, loop_factory) + _make_asyncio_fixture_function(synchronizer, loop_scope, loop_factory) with MonkeyPatch.context() as c: c.setattr(fixturedef, "func", synchronizer) hook_result = yield @@ -798,12 +791,9 @@ def _get_marked_loop_scope( ) -> _ScopeName: assert asyncio_marker.name == "asyncio" if asyncio_marker.args or ( - asyncio_marker.kwargs - and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factory"} + asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factory"} ): - raise ValueError( - "mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'." - ) + raise ValueError("mark.asyncio accepts only a keyword arguments 'loop_scope' or 'loop_factory'") if "scope" in asyncio_marker.kwargs: if "loop_scope" in asyncio_marker.kwargs: raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) From 69bfe77ca672cc82d1ba068d82d049914241484b Mon Sep 17 00:00:00 2001 From: Vizonex Date: Mon, 28 Jul 2025 12:03:02 -0500 Subject: [PATCH 07/12] inject at the runner instead however there was a side-effect so I made a comment explaining it. --- pytest_asyncio/plugin.py | 86 ++++++++++++++++++++++---------------- tests/test_asyncio_mark.py | 10 +++++ 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index ed8f8194..43750e8a 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -135,7 +135,7 @@ def fixture( *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., loop_scope: _ScopeName | None = ..., - loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., + loop_factory: Callable[[], AbstractEventLoop] | None = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: ( @@ -153,7 +153,7 @@ def fixture( *, scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., loop_scope: _ScopeName | None = ..., - loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., + loop_factory: Callable[[], AbstractEventLoop] | None = ..., params: Iterable[object] | None = ..., autouse: bool = ..., ids: ( @@ -168,7 +168,7 @@ def fixture( def fixture( fixture_function: FixtureFunction[_P, _R] | None = None, loop_scope: _ScopeName | None = None, - loop_factory: _ScopeName | Callable[[], AbstractEventLoop] = ..., + loop_factory: Callable[[], AbstractEventLoop] | None = None, **kwargs: Any, ) -> ( FixtureFunction[_P, _R] @@ -192,7 +192,11 @@ def _is_asyncio_fixture_function(obj: Any) -> bool: return getattr(obj, "_force_asyncio_fixture", False) -def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None, loop_factory: _ScopeName | None) -> None: +def _make_asyncio_fixture_function( + obj: Any, + loop_scope: _ScopeName | None, + loop_factory: Callable[[], AbstractEventLoop] | None, +) -> None: if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ @@ -285,14 +289,16 @@ def pytest_report_header(config: Config) -> list[str]: def _fixture_synchronizer( - fixturedef: FixtureDef, runner: Runner, request: FixtureRequest, loop_factory: Callable[[], AbstractEventLoop] + fixturedef: FixtureDef, + runner: Runner, + request: FixtureRequest, ) -> Callable: """Returns a synchronous function evaluating the specified fixture.""" fixture_function = resolve_fixture_function(fixturedef, request) if inspect.isasyncgenfunction(fixturedef.func): - return _wrap_asyncgen_fixture(fixture_function, runner, request, loop_factory) # type: ignore[arg-type] + return _wrap_asyncgen_fixture(fixture_function, runner, request) # type: ignore[arg-type] elif inspect.iscoroutinefunction(fixturedef.func): - return _wrap_async_fixture(fixture_function, runner, request, loop_factory) # type: ignore[arg-type] + return _wrap_async_fixture(fixture_function, runner, request) # type: ignore[arg-type] else: return fixturedef.func @@ -307,7 +313,6 @@ def _wrap_asyncgen_fixture( ], runner: Runner, request: FixtureRequest, - loop_factory:Callable[[], AbstractEventLoop] ) -> Callable[AsyncGenFixtureParams, AsyncGenFixtureYieldType]: @functools.wraps(fixture_function) def _asyncgen_fixture_wrapper( @@ -337,9 +342,6 @@ async def async_finalizer() -> None: msg = "Async generator fixture didn't stop." msg += "Yield only once." raise ValueError(msg) - if loop_factory: - _loop = loop_factory() - asyncio.set_event_loop(_loop) runner.run(async_finalizer(), context=context) if reset_contextvars is not None: @@ -361,9 +363,8 @@ def _wrap_async_fixture( ], runner: Runner, request: FixtureRequest, - loop_factory: Callable[[], AbstractEventLoop] | None = None ) -> Callable[AsyncFixtureParams, AsyncFixtureReturnType]: - @functools.wraps(fixture_function) + @functools.wraps(fixture_function) # type: ignore[arg-type] def _async_fixture_wrapper( *args: AsyncFixtureParams.args, **kwargs: AsyncFixtureParams.kwargs, @@ -374,10 +375,6 @@ async def setup(): context = contextvars.copy_context() - # ensure loop_factory gets ran before we start running... - if loop_factory: - asyncio.set_event_loop(loop_factory()) - result = runner.run(setup(), context=context) # Copy the context vars modified by the setup task into the current # context, and (if needed) add a finalizer to reset them. @@ -522,16 +519,6 @@ def _can_substitute(item: Function) -> bool: func = item.obj return inspect.iscoroutinefunction(func) -<<<<<<< HEAD -======= - def runtest(self) -> None: - # print(self.obj.pytestmark[0].__dict__) - synchronized_obj = wrap_in_sync(self.obj, self.obj.pytestmark[0].kwargs.get('loop_factory', None)) - with MonkeyPatch.context() as c: - c.setattr(self, "obj", synchronized_obj) - super().runtest() - ->>>>>>> edfbfef (figured out loop_factory :)) class AsyncGenerator(PytestAsyncioFunction): """Pytest item created by an asynchronous generator""" @@ -641,16 +628,32 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( @contextlib.contextmanager -def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]: +def _temporary_event_loop_policy( + policy: AbstractEventLoopPolicy, + loop_facotry: Callable[..., AbstractEventLoop] | None, +) -> Iterator[None]: + old_loop_policy = _get_event_loop_policy() try: old_loop = _get_event_loop_no_warn() except RuntimeError: old_loop = None + # XXX: For some reason this function can override runner's + # _loop_factory (At least observed on backported versions of Runner) + # so we need to re-override if existing... + if loop_facotry: + _loop = loop_facotry() + _set_event_loop(_loop) + else: + _loop = None + _set_event_loop_policy(policy) try: yield finally: + if _loop: + # Do not let BaseEventLoop.__del__ complain! + _loop.close() _set_event_loop_policy(old_loop_policy) _set_event_loop(old_loop) @@ -742,8 +745,6 @@ def _synchronize_coroutine( def inner(*args, **kwargs): coro = func(*args, **kwargs) runner.run(coro, context=context) - - asyncio.set_event_loop(_last_loop) return inner @@ -767,7 +768,7 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: runner_fixture_id = f"_{loop_scope}_scoped_runner" runner = request.getfixturevalue(runner_fixture_id) - synchronizer = _fixture_synchronizer(fixturedef, runner, request, loop_factory) + synchronizer = _fixture_synchronizer(fixturedef, runner, request) _make_asyncio_fixture_function(synchronizer, loop_scope, loop_factory) with MonkeyPatch.context() as c: c.setattr(fixturedef, "func", synchronizer) @@ -822,19 +823,32 @@ def _get_default_test_loop_scope(config: Config) -> Any: """ +def _get_loop_facotry( + request: FixtureRequest, +) -> Callable[[], AbstractEventLoop] | None: + if asyncio_mark := request._pyfuncitem.get_closest_marker("asyncio"): + factory = asyncio_mark.kwargs.get("loop_factory", None) + print(f"FACTORY {factory}") + return factory + else: + return request.obj.__dict__.get("_loop_factory", None) # type: ignore[attr-defined] + + def _create_scoped_runner_fixture(scope: _ScopeName) -> Callable: @pytest.fixture( scope=scope, name=f"_{scope}_scoped_runner", ) def _scoped_runner( - event_loop_policy, - request: FixtureRequest, + event_loop_policy: AbstractEventLoopPolicy, request: FixtureRequest ) -> Iterator[Runner]: new_loop_policy = event_loop_policy - debug_mode = _get_asyncio_debug(request.config) - with _temporary_event_loop_policy(new_loop_policy): - runner = Runner(debug=debug_mode).__enter__() + + # We need to get the factory now because + # _temporary_event_loop_policy can override the Runner + factory = _get_loop_facotry(request) + with _temporary_event_loop_policy(new_loop_policy, factory): + runner = Runner(loop_factory=factory).__enter__() try: yield runner except Exception as e: diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 094093c3..0e839bbc 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -249,6 +249,16 @@ class CustomEventLoop(asyncio.SelectorEventLoop): @pytest.mark.asyncio(loop_factory=CustomEventLoop) async def test_has_different_event_loop(): assert type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" + + @pytest_asyncio.fixture(loop_factory=CustomEventLoop) + async def custom_fixture(): + yield asyncio.get_running_loop() + + async def test_with_fixture(custom_fixture): + # Both of these should be the same... + type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" + type(custom_fixture).__name__ == "CustomEventLoop" + """ ) ) From fdaa23c08904955a49d9e4002b1a2deec85a46c6 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Mon, 28 Jul 2025 12:15:43 -0500 Subject: [PATCH 08/12] reformat errors and try and match the new one about loop_factory existing --- tests/markers/test_invalid_arguments.py | 6 ++++-- tests/modes/test_strict_mode.py | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/markers/test_invalid_arguments.py b/tests/markers/test_invalid_arguments.py index a7e499a3..fc2c88f1 100644 --- a/tests/markers/test_invalid_arguments.py +++ b/tests/markers/test_invalid_arguments.py @@ -61,7 +61,8 @@ async def test_anything(): result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ - "*ValueError: mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'*" + "*ValueError: mark.asyncio accepts only a keyword arguments " + "'loop_scope' or 'loop_factory'*" ] ) @@ -84,6 +85,7 @@ async def test_anything(): result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ - "*ValueError: mark.asyncio accepts only keyword arguments 'loop_scope', 'loop_factory'*" + "*ValueError: mark.asyncio accepts only a keyword arguments " + "'loop_scope' or 'loop_factory'*" ] ) diff --git a/tests/modes/test_strict_mode.py b/tests/modes/test_strict_mode.py index d7dc4ac6..ecfd03da 100644 --- a/tests/modes/test_strict_mode.py +++ b/tests/modes/test_strict_mode.py @@ -163,10 +163,7 @@ async def test_anything(any_fixture): result.stdout.fnmatch_lines( [ "*warnings summary*", - ( - "test_strict_mode_marked_test_unmarked_fixture_warning.py::" - "test_anything" - ), + ("test_strict_mode_marked_test_unmarked_fixture_warning.py::test_anything"), ( "*/pytest_asyncio/plugin.py:*: PytestDeprecationWarning: " "asyncio test 'test_anything' requested async " From e16a1d4a8fa8e2e08d417192c5910da35f520858 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Mon, 28 Jul 2025 12:17:46 -0500 Subject: [PATCH 09/12] update changelog with more accurate information --- changelog.d/1164.added.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1164.added.rst b/changelog.d/1164.added.rst index e4cf9e53..f6e6612b 100644 --- a/changelog.d/1164.added.rst +++ b/changelog.d/1164.added.rst @@ -1 +1 @@ -Added ``new_event_loop`` fixture to provide an alternative approch to event loop policies being deprecated +Added ``loop_factory`` to pytest_asyncio.fixture and asyncio mark From e51cd4dbeef3181fd978d5d724307a2eea7f29fb Mon Sep 17 00:00:00 2001 From: Vizonex Date: Mon, 28 Jul 2025 12:20:00 -0500 Subject: [PATCH 10/12] other commits outside of this fork screwed this up, let me fix that... --- pytest_asyncio/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 43750e8a..590bafbd 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -847,8 +847,10 @@ def _scoped_runner( # We need to get the factory now because # _temporary_event_loop_policy can override the Runner factory = _get_loop_facotry(request) + debug_mode = _get_asyncio_debug(request.config) with _temporary_event_loop_policy(new_loop_policy, factory): - runner = Runner(loop_factory=factory).__enter__() + runner = Runner(debug=debug_mode, loop_factory=factory).__enter__() + try: yield runner except Exception as e: From b0349c2fcff82adf700f58277ad72e6e0466455a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 21 Sep 2025 08:10:30 +0200 Subject: [PATCH 11/12] fix remaining tests. --- pytest_asyncio/plugin.py | 24 ++++++++++++++++++------ tests/test_asyncio_mark.py | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 590bafbd..f0b0ccc4 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -49,7 +49,6 @@ PytestPluginManager, ) -from typing import Callable if sys.version_info >= (3, 10): from typing import ParamSpec else: @@ -182,7 +181,12 @@ def fixture( @functools.wraps(fixture) def inner(fixture_function: FixtureFunction[_P, _R]) -> FixtureFunction[_P, _R]: - return fixture(fixture_function, loop_factory=loop_factory, loop_scope=loop_scope, **kwargs) + return fixture( + fixture_function, + loop_factory=loop_factory, + loop_scope=loop_scope, + **kwargs, + ) return inner @@ -741,10 +745,12 @@ def _synchronize_coroutine( Return a sync wrapper around a coroutine executing it in the specified runner and context. """ + @functools.wraps(func) def inner(*args, **kwargs): coro = func(*args, **kwargs) runner.run(coro, context=context) + return inner @@ -792,9 +798,13 @@ def _get_marked_loop_scope( ) -> _ScopeName: assert asyncio_marker.name == "asyncio" if asyncio_marker.args or ( - asyncio_marker.kwargs and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factory"} + asyncio_marker.kwargs + and set(asyncio_marker.kwargs) - {"loop_scope", "scope", "loop_factory"} ): - raise ValueError("mark.asyncio accepts only a keyword arguments 'loop_scope' or 'loop_factory'") + raise ValueError( + "mark.asyncio accepts only a keyword arguments 'loop_scope' " + "or 'loop_factory'" + ) if "scope" in asyncio_marker.kwargs: if "loop_scope" in asyncio_marker.kwargs: raise pytest.UsageError(_DUPLICATE_LOOP_SCOPE_DEFINITION_ERROR) @@ -827,11 +837,13 @@ def _get_loop_facotry( request: FixtureRequest, ) -> Callable[[], AbstractEventLoop] | None: if asyncio_mark := request._pyfuncitem.get_closest_marker("asyncio"): + # The loop_factory is defined on an asyncio marker factory = asyncio_mark.kwargs.get("loop_factory", None) - print(f"FACTORY {factory}") return factory else: - return request.obj.__dict__.get("_loop_factory", None) # type: ignore[attr-defined] + # The loop_factory is pulled in via a fixture + top_request = list(request._iter_chain())[-1]._parent_request + return top_request._pyfuncitem.__dict__.get("_loop_factory", None) # type: ignore[attr-defined] def _create_scoped_runner_fixture(scope: _ScopeName) -> Callable: diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index 0e839bbc..dd328711 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -264,4 +264,4 @@ async def test_with_fixture(custom_fixture): ) result = pytester.runpytest("--asyncio-mode=auto") - result.assert_outcomes(passed=1) + result.assert_outcomes(passed=2) From e1af98a5f9b33d59e5e2644300e454409ebb08af Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Wed, 24 Sep 2025 21:23:00 +0200 Subject: [PATCH 12/12] wip: Add tests for additional edge cases and try to cover them. --- pytest_asyncio/plugin.py | 26 +- .../test_async_fixtures_contextvars.py | 2 +- tests/test_asyncio_mark.py | 229 +++++++++++++++--- 3 files changed, 214 insertions(+), 43 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f0b0ccc4..0921c7d6 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -836,14 +836,28 @@ def _get_default_test_loop_scope(config: Config) -> Any: def _get_loop_facotry( request: FixtureRequest, ) -> Callable[[], AbstractEventLoop] | None: - if asyncio_mark := request._pyfuncitem.get_closest_marker("asyncio"): + loop_factories = [] + asyncio_mark = request._pyfuncitem.get_closest_marker("asyncio") + if asyncio_mark is not None: # The loop_factory is defined on an asyncio marker factory = asyncio_mark.kwargs.get("loop_factory", None) - return factory - else: - # The loop_factory is pulled in via a fixture - top_request = list(request._iter_chain())[-1]._parent_request - return top_request._pyfuncitem.__dict__.get("_loop_factory", None) # type: ignore[attr-defined] + loop_factories.append(factory) + # The loop_factory is defined in a transitive fixture + current_request = request + for r in request._iter_chain(): + current_request = r + loop_factory = getattr(current_request._fixturedef.func, "_loop_factory", None) + loop_factories.append(loop_factory) + defined_loop_factories = [factory for factory in loop_factories if factory] or [ + None + ] + print(defined_loop_factories) + if len(defined_loop_factories) > 1: + print(defined_loop_factories) + raise pytest.UsageError( + "Multiple loop factories defined for {request.scope}-scoped loop." + ) + return defined_loop_factories[0] def _create_scoped_runner_fixture(scope: _ScopeName) -> Callable: diff --git a/tests/async_fixtures/test_async_fixtures_contextvars.py b/tests/async_fixtures/test_async_fixtures_contextvars.py index e8634d0c..bfff5f74 100644 --- a/tests/async_fixtures/test_async_fixtures_contextvars.py +++ b/tests/async_fixtures/test_async_fixtures_contextvars.py @@ -101,7 +101,7 @@ def test(check_var_fixture): """ ) ) - result = pytester.runpytest("--asyncio-mode=strict") + result = pytester.runpytest("--asyncio-mode=strict", "-s") result.assert_outcomes(passed=1) diff --git a/tests/test_asyncio_mark.py b/tests/test_asyncio_mark.py index dd328711..26a12319 100644 --- a/tests/test_asyncio_mark.py +++ b/tests/test_asyncio_mark.py @@ -148,7 +148,7 @@ async def test_a(): ) -def test_asyncio_marker_fallbacks_to_configured_default_loop_scope_if_not_set( +def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set( pytester: Pytester, ): pytester.makeini( @@ -156,7 +156,7 @@ def test_asyncio_marker_fallbacks_to_configured_default_loop_scope_if_not_set( """\ [pytest] asyncio_default_fixture_loop_scope = function - asyncio_default_test_loop_scope = session + asyncio_default_test_loop_scope = module """ ) ) @@ -175,6 +175,7 @@ async def session_loop_fixture(): global loop loop = asyncio.get_running_loop() + @pytest.mark.asyncio(loop_scope="session") async def test_a(session_loop_fixture): global loop assert asyncio.get_running_loop() is loop @@ -186,19 +187,34 @@ async def test_a(session_loop_fixture): result.assert_outcomes(passed=1) -def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set( - pytester: Pytester, -): - pytester.makeini( +def test_uses_loop_factory_from_test(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ - [pytest] - asyncio_default_fixture_loop_scope = function - asyncio_default_test_loop_scope = module + import asyncio + import pytest_asyncio + import pytest + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest_asyncio.fixture(loop_scope="module") + async def any_fixture(): + assert type(asyncio.get_running_loop()) == CustomEventLoop + + @pytest.mark.asyncio(loop_scope="module", loop_factory=CustomEventLoop) + async def test_set_loop_factory(any_fixture): + assert type(asyncio.get_running_loop()) == CustomEventLoop """ ) ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + +def test_uses_loop_factory_from_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -206,36 +222,90 @@ def test_asyncio_marker_uses_marker_loop_scope_even_if_config_is_set( import pytest_asyncio import pytest - loop: asyncio.AbstractEventLoop + class CustomEventLoop(asyncio.SelectorEventLoop): + pass - @pytest_asyncio.fixture(loop_scope="session", scope="session") - async def session_loop_fixture(): - global loop - loop = asyncio.get_running_loop() + @pytest_asyncio.fixture(loop_scope="module", loop_factory=CustomEventLoop) + async def any_fixture(): + assert type(asyncio.get_running_loop()) == CustomEventLoop - @pytest.mark.asyncio(loop_scope="session") - async def test_a(session_loop_fixture): - global loop - assert asyncio.get_running_loop() is loop + @pytest.mark.asyncio(loop_scope="module") + async def test_set_loop_factory(any_fixture): + assert type(asyncio.get_running_loop()) == CustomEventLoop """ ) ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1) - result = pytester.runpytest("--asyncio-mode=auto") + +def test_uses_loop_factory_from_transitive_fixture(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest_asyncio + import pytest + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest_asyncio.fixture(loop_scope="module", loop_factory=CustomEventLoop) + async def transitive_fixture(): + assert type(asyncio.get_running_loop()) == CustomEventLoop + + @pytest_asyncio.fixture(loop_scope="module") + async def any_fixture(transitive_fixture): + assert type(asyncio.get_running_loop()) == CustomEventLoop + + @pytest.mark.asyncio(loop_scope="module") + async def test_set_loop_factory(any_fixture): + assert type(asyncio.get_running_loop()) == CustomEventLoop + """ + ) + ) + result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=1) -def test_asyncio_marker_event_loop_factories(pytester: Pytester): - pytester.makeini( +def test_conflicting_loop_factories_in_tests_raise_error(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( dedent( """\ - [pytest] - asyncio_default_fixture_loop_scope = function - asyncio_default_test_loop_scope = module + import asyncio + import pytest_asyncio + import pytest + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + class AnotherCustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest.mark.asyncio(loop_scope="module", loop_factory=CustomEventLoop) + async def test_with_custom_loop_factory(): + ... + + @pytest.mark.asyncio( + loop_scope="module", + loop_factory=AnotherCustomEventLoop + ) + async def test_with_a_different_custom_loop_factory(): + ... """ ) ) + result = pytester.runpytest("--asyncio-mode=strict", "-s", "--setup-show") + result.assert_outcomes(errors=2) + + +def test_conflicting_loop_factories_in_tests_and_fixtures_raise_error( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( dedent( """\ @@ -246,22 +316,109 @@ def test_asyncio_marker_event_loop_factories(pytester: Pytester): class CustomEventLoop(asyncio.SelectorEventLoop): pass - @pytest.mark.asyncio(loop_factory=CustomEventLoop) - async def test_has_different_event_loop(): - assert type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" + class AnotherCustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest_asyncio.fixture(loop_scope="module", loop_factory=CustomEventLoop) + async def fixture_with_custom_loop_factory(): + ... + + @pytest.mark.asyncio( + loop_scope="module", + loop_factory=AnotherCustomEventLoop + ) + async def test_trying_to_override_fixtures_loop_factory( + fixture_with_custom_loop_factory + ): + # Fails, because it tries to use a different loop factory on the + # same runner as the first test + ... + """ + ) + ) + + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, errors=1) - @pytest_asyncio.fixture(loop_factory=CustomEventLoop) - async def custom_fixture(): - yield asyncio.get_running_loop() - async def test_with_fixture(custom_fixture): - # Both of these should be the same... - type(asyncio.get_running_loop()).__name__ == "CustomEventLoop" - type(custom_fixture).__name__ == "CustomEventLoop" +def test_conflicting_loop_factories_in_fixtures_raise_error(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest_asyncio + import pytest + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + class AnotherCustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest_asyncio.fixture(loop_scope="module", loop_factory=CustomEventLoop) + async def fixture_with_custom_loop_factory(): + ... + + @pytest_asyncio.fixture( + loop_scope="module", + loop_factory=AnotherCustomEventLoop + ) + async def another_fixture_with_custom_loop_factory(): + ... + + @pytest.mark.asyncio(loop_scope="module") + async def test_requesting_two_fixtures_with_different_loop_facoties( + fixture_with_custom_loop_factory, + another_fixture_with_custom_loop_factory, + ): + ... """ ) ) - result = pytester.runpytest("--asyncio-mode=auto") - result.assert_outcomes(passed=2) + result = pytester.runpytest("--asyncio-mode=strict", "-s", "--setup-show") + result.assert_outcomes(errors=1) + + +def test_conflicting_loop_factories_in_transitive_fixtures_raise_error( + pytester: Pytester, +): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile( + dedent( + """\ + import asyncio + import pytest_asyncio + import pytest + + class CustomEventLoop(asyncio.SelectorEventLoop): + pass + + class AnotherCustomEventLoop(asyncio.SelectorEventLoop): + pass + + @pytest_asyncio.fixture(loop_scope="module", loop_factory=CustomEventLoop) + async def fixture_with_custom_loop_factory(): + ... + + @pytest_asyncio.fixture( + loop_scope="module", + loop_factory=AnotherCustomEventLoop + ) + async def another_fixture_with_custom_loop_factory( + fixture_with_custom_loop_factory + ): + ... + + @pytest.mark.asyncio(loop_scope="module") + async def test_requesting_two_fixtures_with_different_loop_facories( + another_fixture_with_custom_loop_factory, + ): + ... + """ + ) + ) + + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(errors=1)