From 03c9ab88b45dabe46c5f7e93c085b212c0686c31 Mon Sep 17 00:00:00 2001 From: sainekk Date: Fri, 5 Jun 2026 02:21:48 +0300 Subject: [PATCH] Deprecate @pytest_asyncio.fixture for synchronous fixtures Widen _R TypeVar (removing the Awaitable|AsyncIterator bound) so that @pytest_asyncio.fixture can be applied to synchronous functions without a type error. Emit PytestDeprecationWarning at decoration time when the decorated function is not a coroutine or async generator function. The internal synchronizer wrapper in pytest_fixture_setup bypasses _make_asyncio_fixture_function to avoid a false-positive warning. Closes #1090 --- changelog.d/1090.deprecated.rst | 1 + pytest_asyncio/plugin.py | 20 ++++++++++--- tests/test_asyncio_fixture.py | 52 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1090.deprecated.rst diff --git a/changelog.d/1090.deprecated.rst b/changelog.d/1090.deprecated.rst new file mode 100644 index 00000000..0769275b --- /dev/null +++ b/changelog.d/1090.deprecated.rst @@ -0,0 +1 @@ +Using :func:`pytest_asyncio.fixture` to decorate synchronous fixtures is now deprecated. Use :func:`pytest.fixture` instead. diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2fe8db12..c4f12be5 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -14,8 +14,6 @@ import warnings from asyncio import AbstractEventLoop from collections.abc import ( - AsyncIterator, - Awaitable, Callable, Collection, Generator, @@ -69,7 +67,7 @@ from asyncio import AbstractEventLoopPolicy _ScopeName = Literal["session", "package", "module", "class", "function"] -_R = TypeVar("_R", bound=Awaitable[Any] | AsyncIterator[Any]) +_R = TypeVar("_R") _P = ParamSpec("_P") FixtureFunction = Callable[_P, _R] LoopFactory: TypeAlias = Callable[[], AbstractEventLoop] @@ -211,6 +209,19 @@ def _make_asyncio_fixture_function(obj: Any, loop_scope: _ScopeName | None) -> N if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ + if not _is_coroutine_or_asyncgen(obj): + warnings.warn( + PytestDeprecationWarning( + "@pytest_asyncio.fixture was applied to the synchronous function " + f"{obj.__name__!r}. Use @pytest.fixture for synchronous fixtures. " + "This will become an error in future versions of pytest-asyncio." + ), + stacklevel=3, + # stacklevel=3 points at the user's @pytest_asyncio.fixture line for + # the bare-decorator path (user → fixture() → here). The factory path + # (@pytest_asyncio.fixture(...)) adds one extra frame via inner(), so + # the warning lands on plugin.py:inner instead — still the plugin. + ) obj._force_asyncio_fixture = True obj._loop_scope = loop_scope @@ -938,7 +949,8 @@ def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None: functools.partial(fixturedef.finish, request=request) ) synchronizer = _fixture_synchronizer(fixturedef, runner, request) - _make_asyncio_fixture_function(synchronizer, loop_scope) + synchronizer._force_asyncio_fixture = True # type: ignore[attr-defined] + synchronizer._loop_scope = loop_scope # type: ignore[attr-defined] with MonkeyPatch.context() as c: c.setattr(fixturedef, "func", synchronizer) hook_result = yield diff --git a/tests/test_asyncio_fixture.py b/tests/test_asyncio_fixture.py index 32cd7d8c..3cffb4bf 100644 --- a/tests/test_asyncio_fixture.py +++ b/tests/test_asyncio_fixture.py @@ -62,3 +62,55 @@ def test_sync_function_uses_async_fixture(always_true): """)) result = pytester.runpytest(f"--asyncio-mode={mode}") result.assert_outcomes(passed=1) + + +def test_sync_fixture_emits_deprecation_warning(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture + def sync_fixture(): + return 42 + + @pytest.mark.asyncio + async def test_uses_sync(sync_fixture): + assert sync_fixture == 42 + """)) + result = pytester.runpytest("-W", "default") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + ( + "*PytestDeprecationWarning*@pytest_asyncio.fixture*" + "*sync_fixture*@pytest.fixture*" + ) + ] + ) + + +def test_sync_fixture_factory_path_emits_deprecation_warning(pytester: Pytester): + pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") + pytester.makepyfile(dedent("""\ + import pytest + import pytest_asyncio + + @pytest_asyncio.fixture(loop_scope="function") + def sync_fixture(): + return 42 + + @pytest.mark.asyncio + async def test_uses_sync(sync_fixture): + assert sync_fixture == 42 + """)) + result = pytester.runpytest("-W", "default") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + [ + ( + "*PytestDeprecationWarning*@pytest_asyncio.fixture*" + "*sync_fixture*@pytest.fixture*" + ) + ] + )