From d871b508011011cd2c76a7aa354b569cef38b24b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 29 Nov 2025 22:23:13 +0200 Subject: [PATCH] fixtures: turn requesting async fixture without a plugin into a hard error Deprecated feature scheduled for removal in pytest 9. Part of #13893. --- doc/en/deprecations.rst | 142 +++++++++++++++++++------------------ src/_pytest/fixtures.py | 18 ++--- testing/acceptance_test.py | 50 +++++-------- 3 files changed, 93 insertions(+), 117 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 7f7e3536655..e607b7f26dc 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -148,76 +148,6 @@ Simply remove the ``__init__.py`` file entirely. Python 3.3+ natively supports namespace packages without ``__init__.py``. -.. _sync-test-async-fixture: - -sync test depending on async fixture -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.4 - -Pytest has for a long time given an error when encountering an asynchronous test function, prompting the user to install -a plugin that can handle it. It has not given any errors if you have an asynchronous fixture that's depended on by a -synchronous test. If the fixture was an async function you did get an "unawaited coroutine" warning, but for async yield fixtures you didn't even get that. -This is a problem even if you do have a plugin installed for handling async tests, as they may require -special decorators for async fixtures to be handled, and some may not robustly handle if a user accidentally requests an -async fixture from their sync tests. Fixture values being cached can make this even more unintuitive, where everything will -"work" if the fixture is first requested by an async test, and then requested by a synchronous test. - -Unfortunately there is no 100% reliable method of identifying when a user has made a mistake, versus when they expect an -unawaited object from their fixture that they will handle on their own. To suppress this warning -when you in fact did intend to handle this you can wrap your async fixture in a synchronous fixture: - -.. code-block:: python - - import asyncio - import pytest - - - @pytest.fixture - async def unawaited_fixture(): - return 1 - - - def test_foo(unawaited_fixture): - assert 1 == asyncio.run(unawaited_fixture) - -should be changed to - - -.. code-block:: python - - import asyncio - import pytest - - - @pytest.fixture - def unawaited_fixture(): - async def inner_fixture(): - return 1 - - return inner_fixture() - - - def test_foo(unawaited_fixture): - assert 1 == asyncio.run(unawaited_fixture) - - -You can also make use of `pytest_fixture_setup` to handle the coroutine/asyncgen before pytest sees it - this is the way current async pytest plugins handle it. - -If a user has an async fixture with ``autouse=True`` in their ``conftest.py``, or in a file -containing both synchronous tests and the fixture, they will receive this warning. -Unless you're using a plugin that specifically handles async fixtures -with synchronous tests, we strongly recommend against this practice. -It can lead to unpredictable behavior (with larger scopes, it may appear to "work" if an async -test is the first to request the fixture, due to value caching) and will generate -unawaited-coroutine runtime warnings (but only for non-yield fixtures). -Additionally, it creates ambiguity for other developers about whether the fixture is intended to perform -setup for synchronous tests. - -The `anyio pytest plugin `_ supports -synchronous tests with async fixtures, though certain limitations apply. - - .. _import-or-skip-import-error: ``pytest.importorskip`` default behavior regarding :class:`ImportError` @@ -423,6 +353,78 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. +.. _sync-test-async-fixture: + +sync test depending on async fixture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4 +.. versionremoved:: 9.0 + +Pytest has for a long time given an error when encountering an asynchronous test function, prompting the user to install +a plugin that can handle it. It has not given any errors if you have an asynchronous fixture that's depended on by a +synchronous test. If the fixture was an async function you did get an "unawaited coroutine" warning, but for async yield fixtures you didn't even get that. +This is a problem even if you do have a plugin installed for handling async tests, as they may require +special decorators for async fixtures to be handled, and some may not robustly handle if a user accidentally requests an +async fixture from their sync tests. Fixture values being cached can make this even more unintuitive, where everything will +"work" if the fixture is first requested by an async test, and then requested by a synchronous test. + +Unfortunately there is no 100% reliable method of identifying when a user has made a mistake, versus when they expect an +unawaited object from their fixture that they will handle on their own. To suppress this warning +when you in fact did intend to handle this you can wrap your async fixture in a synchronous fixture: + +.. code-block:: python + + import asyncio + import pytest + + + @pytest.fixture + async def unawaited_fixture(): + return 1 + + + def test_foo(unawaited_fixture): + assert 1 == asyncio.run(unawaited_fixture) + +should be changed to + + +.. code-block:: python + + import asyncio + import pytest + + + @pytest.fixture + def unawaited_fixture(): + async def inner_fixture(): + return 1 + + return inner_fixture() + + + def test_foo(unawaited_fixture): + assert 1 == asyncio.run(unawaited_fixture) + + +You can also make use of `pytest_fixture_setup` to handle the coroutine/asyncgen before pytest sees it - this is the way current async pytest plugins handle it. + +If a user has an async fixture with ``autouse=True`` in their ``conftest.py``, or in a file +containing both synchronous tests and the fixture, they will receive this warning. +Unless you're using a plugin that specifically handles async fixtures +with synchronous tests, we strongly recommend against this practice. +It can lead to unpredictable behavior (with larger scopes, it may appear to "work" if an async +test is the first to request the fixture, due to value caching) and will generate +unawaited-coroutine runtime warnings (but only for non-yield fixtures). +Additionally, it creates ambiguity for other developers about whether the fixture is intended to perform +setup for synchronous tests. + +The `anyio pytest plugin `_ supports +synchronous tests with async fixtures, though certain limitations apply. + + + Applying a mark to a fixture function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bb9daa84726..d8d19fcac6d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -66,7 +66,6 @@ from _pytest.scope import _ScopeName from _pytest.scope import HIGH_SCOPES from _pytest.scope import Scope -from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import PytestWarning @@ -1178,18 +1177,11 @@ def pytest_fixture_setup( fixturefunc ): auto_str = " with autouse=True" if fixturedef._autouse else "" - - warnings.warn( - PytestRemovedIn9Warning( - f"{request.node.name!r} requested an async fixture " - f"{request.fixturename!r}{auto_str}, with no plugin or hook that " - "handled it. This is usually an error, as pytest does not natively " - "support it. " - "This will turn into an error in pytest 9.\n" - "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture" - ), - # no stacklevel will point at users code, so we just point here - stacklevel=1, + fail( + f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, " + "with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n" + "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + pytrace=False, ) try: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 11cc8a7217f..f941cbe1921 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1307,7 +1307,7 @@ def test_3(): result.assert_outcomes(failed=3) -def test_warning_on_sync_test_async_fixture(pytester: Pytester) -> None: +def test_error_on_sync_test_async_fixture(pytester: Pytester) -> None: pytester.makepyfile( test_sync=""" import pytest @@ -1324,23 +1324,17 @@ def test_foo(async_fixture): pass """ ) - result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning") + result = pytester.runpytest() + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ - "*== warnings summary ==*", - ( - "*PytestRemovedIn9Warning: 'test_foo' requested an async " - "fixture 'async_fixture', with no plugin or hook that handled it. " - "This is usually an error, as pytest does not natively support it. " - "This will turn into an error in pytest 9." - ), - " See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + "'test_foo' requested an async fixture 'async_fixture', with no plugin or hook that handled it. " + "This is an error, as pytest does not natively support it." ] ) - result.assert_outcomes(passed=1, warnings=1) -def test_warning_on_sync_test_async_fixture_gen(pytester: Pytester) -> None: +def test_error_on_sync_test_async_fixture_gen(pytester: Pytester) -> None: pytester.makepyfile( test_sync=""" import pytest @@ -1354,23 +1348,17 @@ def test_foo(async_fixture): ... """ ) - result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning") + result = pytester.runpytest() + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ - "*== warnings summary ==*", - ( - "*PytestRemovedIn9Warning: 'test_foo' requested an async " - "fixture 'async_fixture', with no plugin or hook that handled it. " - "This is usually an error, as pytest does not natively support it. " - "This will turn into an error in pytest 9." - ), - " See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + "'test_foo' requested an async fixture 'async_fixture', with no plugin or hook that handled it. " + "This is an error, as pytest does not natively support it." ] ) - result.assert_outcomes(passed=1, warnings=1) -def test_warning_on_sync_test_async_autouse_fixture(pytester: Pytester) -> None: +def test_error_on_sync_test_async_autouse_fixture(pytester: Pytester) -> None: pytester.makepyfile( test_sync=""" import pytest @@ -1388,21 +1376,15 @@ def test_foo(async_fixture): pass """ ) - result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning") + result = pytester.runpytest() + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( [ - "*== warnings summary ==*", - ( - "*PytestRemovedIn9Warning: 'test_foo' requested an async " - "fixture 'async_fixture' with autouse=True, with no plugin or hook " - "that handled it. " - "This is usually an error, as pytest does not natively support it. " - "This will turn into an error in pytest 9." - ), - " See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture", + "'test_foo' requested an async fixture 'async_fixture' with autouse=True, " + "with no plugin or hook that handled it. " + "This is an error, as pytest does not natively support it." ] ) - result.assert_outcomes(passed=1, warnings=1) def test_pdb_can_be_rewritten(pytester: Pytester) -> None: