Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 72 additions & 70 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://anyio.readthedocs.io/en/stable/testing.html>`_ supports
synchronous tests with async fixtures, though certain limitations apply.


.. _import-or-skip-import-error:

``pytest.importorskip`` default behavior regarding :class:`ImportError`
Expand Down Expand Up @@ -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 <https://anyio.readthedocs.io/en/stable/testing.html>`_ supports
synchronous tests with async fixtures, though certain limitations apply.



Applying a mark to a fixture function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
18 changes: 5 additions & 13 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down
50 changes: 16 additions & 34 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down