Skip to content

Commit 54d8236

Browse files
author
Arpit Gupta
authored
Merge branch 'main' into refactor/remove-is-generator
2 parents 1bcaf2c + 5611bdd commit 54d8236

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
@@ -72,6 +72,7 @@
7272
from _pytest.scope import _ScopeName
7373
from _pytest.scope import HIGH_SCOPES
7474
from _pytest.scope import Scope
75+
from _pytest.warning_types import PytestRemovedIn9Warning
7576

7677

7778
if sys.version_info < (3, 11):
@@ -574,6 +575,7 @@ def _get_active_fixturedef(
574575
# The are no fixtures with this name applicable for the function.
575576
if not fixturedefs:
576577
raise FixtureLookupError(argname, self)
578+
577579
# A fixture may override another fixture with the same name, e.g. a
578580
# fixture in a module can override a fixture in a conftest, a fixture in
579581
# a class can override a fixture in the module, and so on.
@@ -967,6 +969,8 @@ def __init__(
967969
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
968970
*,
969971
_ispytest: bool = False,
972+
# only used in a deprecationwarning msg, can be removed in pytest9
973+
_autouse: bool = False,
970974
) -> None:
971975
check_ispytest(_ispytest)
972976
# The "base" node ID for the fixture.
@@ -1013,6 +1017,9 @@ def __init__(
10131017
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
10141018
self._finalizers: Final[list[Callable[[], object]]] = []
10151019

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

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

16791706
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)