Skip to content

Commit 5611bdd

Browse files
authored
Merge pull request #12930 from jakkdl/sync_test_async_fixture
DeprecationWarning if sync test requests async fixture
2 parents 8691ff1 + 7084940 commit 5611bdd

File tree

5 files changed

+197
-1
lines changed

5 files changed

+197
-1
lines changed

changelog/10839.deprecation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Requesting an asynchronous fixture without a `pytest_fixture_setup` hook that resolves it will now give a DeprecationWarning. This most commonly happens if a sync test requests an async fixture. This should have no effect on a majority of users with async tests or fixtures using async pytest plugins, but may affect non-standard hook setups or ``autouse=True``. For guidance on how to work around this warning see :ref:`sync-test-async-fixture`.

doc/en/deprecations.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,76 @@ Below is a complete list of all pytest features which are considered deprecated.
1515
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
1616

1717

18+
.. _sync-test-async-fixture:
19+
20+
sync test depending on async fixture
21+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22+
23+
.. deprecated:: 8.4
24+
25+
Pytest has for a long time given an error when encountering an asynchronous test function, prompting the user to install
26+
a plugin that can handle it. It has not given any errors if you have an asynchronous fixture that's depended on by a
27+
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.
28+
This is a problem even if you do have a plugin installed for handling async tests, as they may require
29+
special decorators for async fixtures to be handled, and some may not robustly handle if a user accidentally requests an
30+
async fixture from their sync tests. Fixture values being cached can make this even more unintuitive, where everything will
31+
"work" if the fixture is first requested by an async test, and then requested by a synchronous test.
32+
33+
Unfortunately there is no 100% reliable method of identifying when a user has made a mistake, versus when they expect an
34+
unawaited object from their fixture that they will handle on their own. To suppress this warning
35+
when you in fact did intend to handle this you can wrap your async fixture in a synchronous fixture:
36+
37+
.. code-block:: python
38+
39+
import asyncio
40+
import pytest
41+
42+
43+
@pytest.fixture
44+
async def unawaited_fixture():
45+
return 1
46+
47+
48+
def test_foo(unawaited_fixture):
49+
assert 1 == asyncio.run(unawaited_fixture)
50+
51+
should be changed to
52+
53+
54+
.. code-block:: python
55+
56+
import asyncio
57+
import pytest
58+
59+
60+
@pytest.fixture
61+
def unawaited_fixture():
62+
async def inner_fixture():
63+
return 1
64+
65+
return inner_fixture()
66+
67+
68+
def test_foo(unawaited_fixture):
69+
assert 1 == asyncio.run(unawaited_fixture)
70+
71+
72+
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.
73+
74+
If a user has an async fixture with ``autouse=True`` in their ``conftest.py``, or in a file
75+
containing both synchronous tests and the fixture, they will receive this warning.
76+
Unless you're using a plugin that specifically handles async fixtures
77+
with synchronous tests, we strongly recommend against this practice.
78+
It can lead to unpredictable behavior (with larger scopes, it may appear to "work" if an async
79+
test is the first to request the fixture, due to value caching) and will generate
80+
unawaited-coroutine runtime warnings (but only for non-yield fixtures).
81+
Additionally, it creates ambiguity for other developers about whether the fixture is intended to perform
82+
setup for synchronous tests.
83+
84+
The `anyio pytest plugin <https://anyio.readthedocs.io/en/stable/testing.html>`_ supports
85+
synchronous tests with async fixtures, though certain limitations apply.
86+
87+
1888
.. _import-or-skip-import-error:
1989

2090
``pytest.importorskip`` default behavior regarding :class:`ImportError`

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ disable = [
335335
]
336336

337337
[tool.codespell]
338-
ignore-words-list = "afile,asser,assertio,feld,hove,ned,noes,notin,paramete,parth,socio-economic,tesults,varius,wil"
338+
ignore-words-list = "afile,asend,asser,assertio,feld,hove,ned,noes,notin,paramete,parth,socio-economic,tesults,varius,wil"
339339
skip = "*/plugin_list.rst"
340340
write-changes = true
341341

src/_pytest/fixtures.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
from _pytest.scope import _ScopeName
7474
from _pytest.scope import HIGH_SCOPES
7575
from _pytest.scope import Scope
76+
from _pytest.warning_types import PytestRemovedIn9Warning
7677

7778

7879
if sys.version_info < (3, 11):
@@ -575,6 +576,7 @@ def _get_active_fixturedef(
575576
# The are no fixtures with this name applicable for the function.
576577
if not fixturedefs:
577578
raise FixtureLookupError(argname, self)
579+
578580
# A fixture may override another fixture with the same name, e.g. a
579581
# fixture in a module can override a fixture in a conftest, a fixture in
580582
# a class can override a fixture in the module, and so on.
@@ -968,6 +970,8 @@ def __init__(
968970
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
969971
*,
970972
_ispytest: bool = False,
973+
# only used in a deprecationwarning msg, can be removed in pytest9
974+
_autouse: bool = False,
971975
) -> None:
972976
check_ispytest(_ispytest)
973977
# The "base" node ID for the fixture.
@@ -1014,6 +1018,9 @@ def __init__(
10141018
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
10151019
self._finalizers: Final[list[Callable[[], object]]] = []
10161020

1021+
# only used to emit a deprecationwarning, can be removed in pytest9
1022+
self._autouse = _autouse
1023+
10171024
@property
10181025
def scope(self) -> _ScopeName:
10191026
"""Scope string, one of "function", "class", "module", "package", "session"."""
@@ -1145,6 +1152,25 @@ def pytest_fixture_setup(
11451152

11461153
fixturefunc = resolve_fixture_function(fixturedef, request)
11471154
my_cache_key = fixturedef.cache_key(request)
1155+
1156+
if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction(
1157+
fixturefunc
1158+
):
1159+
auto_str = " with autouse=True" if fixturedef._autouse else ""
1160+
1161+
warnings.warn(
1162+
PytestRemovedIn9Warning(
1163+
f"{request.node.name!r} requested an async fixture "
1164+
f"{request.fixturename!r}{auto_str}, with no plugin or hook that "
1165+
"handled it. This is usually an error, as pytest does not natively "
1166+
"support it. "
1167+
"This will turn into an error in pytest 9.\n"
1168+
"See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture"
1169+
),
1170+
# no stacklevel will point at users code, so we just point here
1171+
stacklevel=1,
1172+
)
1173+
11481174
try:
11491175
result = call_fixture_func(fixturefunc, request, kwargs)
11501176
except TEST_OUTCOME as e:
@@ -1675,6 +1701,7 @@ def _register_fixture(
16751701
params=params,
16761702
ids=ids,
16771703
_ispytest=True,
1704+
_autouse=autouse,
16781705
)
16791706

16801707
faclist = self._arg2fixturedefs.setdefault(name, [])

testing/acceptance_test.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,104 @@ def test_3():
12861286
result.assert_outcomes(failed=3)
12871287

12881288

1289+
def test_warning_on_sync_test_async_fixture(pytester: Pytester) -> None:
1290+
pytester.makepyfile(
1291+
test_sync="""
1292+
import pytest
1293+
1294+
@pytest.fixture
1295+
async def async_fixture():
1296+
...
1297+
1298+
def test_foo(async_fixture):
1299+
# suppress unawaited coroutine warning
1300+
try:
1301+
async_fixture.send(None)
1302+
except StopIteration:
1303+
pass
1304+
"""
1305+
)
1306+
result = pytester.runpytest()
1307+
result.stdout.fnmatch_lines(
1308+
[
1309+
"*== warnings summary ==*",
1310+
(
1311+
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1312+
"fixture 'async_fixture', with no plugin or hook that handled it. "
1313+
"This is usually an error, as pytest does not natively support it. "
1314+
"This will turn into an error in pytest 9."
1315+
),
1316+
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1317+
]
1318+
)
1319+
result.assert_outcomes(passed=1, warnings=1)
1320+
1321+
1322+
def test_warning_on_sync_test_async_fixture_gen(pytester: Pytester) -> None:
1323+
pytester.makepyfile(
1324+
test_sync="""
1325+
import pytest
1326+
1327+
@pytest.fixture
1328+
async def async_fixture():
1329+
yield
1330+
1331+
def test_foo(async_fixture):
1332+
# async gens don't emit unawaited-coroutine
1333+
...
1334+
"""
1335+
)
1336+
result = pytester.runpytest()
1337+
result.stdout.fnmatch_lines(
1338+
[
1339+
"*== warnings summary ==*",
1340+
(
1341+
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1342+
"fixture 'async_fixture', with no plugin or hook that handled it. "
1343+
"This is usually an error, as pytest does not natively support it. "
1344+
"This will turn into an error in pytest 9."
1345+
),
1346+
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1347+
]
1348+
)
1349+
result.assert_outcomes(passed=1, warnings=1)
1350+
1351+
1352+
def test_warning_on_sync_test_async_autouse_fixture(pytester: Pytester) -> None:
1353+
pytester.makepyfile(
1354+
test_sync="""
1355+
import pytest
1356+
1357+
@pytest.fixture(autouse=True)
1358+
async def async_fixture():
1359+
...
1360+
1361+
# We explicitly request the fixture to be able to
1362+
# suppress the RuntimeWarning for unawaited coroutine.
1363+
def test_foo(async_fixture):
1364+
try:
1365+
async_fixture.send(None)
1366+
except StopIteration:
1367+
pass
1368+
"""
1369+
)
1370+
result = pytester.runpytest()
1371+
result.stdout.fnmatch_lines(
1372+
[
1373+
"*== warnings summary ==*",
1374+
(
1375+
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1376+
"fixture 'async_fixture' with autouse=True, with no plugin or hook "
1377+
"that handled it. "
1378+
"This is usually an error, as pytest does not natively support it. "
1379+
"This will turn into an error in pytest 9."
1380+
),
1381+
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1382+
]
1383+
)
1384+
result.assert_outcomes(passed=1, warnings=1)
1385+
1386+
12891387
def test_pdb_can_be_rewritten(pytester: Pytester) -> None:
12901388
pytester.makepyfile(
12911389
**{

0 commit comments

Comments
 (0)