Skip to content

Commit 087ac04

Browse files
authored
Merge pull request #14015 from bluetech/async-fixture-hard-error
fixtures: turn requesting async fixture without a plugin into a hard error
2 parents be5c7ed + d871b50 commit 087ac04

File tree

3 files changed

+93
-117
lines changed

3 files changed

+93
-117
lines changed

doc/en/deprecations.rst

Lines changed: 72 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -148,76 +148,6 @@ Simply remove the ``__init__.py`` file entirely.
148148
Python 3.3+ natively supports namespace packages without ``__init__.py``.
149149

150150

151-
.. _sync-test-async-fixture:
152-
153-
sync test depending on async fixture
154-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
155-
156-
.. deprecated:: 8.4
157-
158-
Pytest has for a long time given an error when encountering an asynchronous test function, prompting the user to install
159-
a plugin that can handle it. It has not given any errors if you have an asynchronous fixture that's depended on by a
160-
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.
161-
This is a problem even if you do have a plugin installed for handling async tests, as they may require
162-
special decorators for async fixtures to be handled, and some may not robustly handle if a user accidentally requests an
163-
async fixture from their sync tests. Fixture values being cached can make this even more unintuitive, where everything will
164-
"work" if the fixture is first requested by an async test, and then requested by a synchronous test.
165-
166-
Unfortunately there is no 100% reliable method of identifying when a user has made a mistake, versus when they expect an
167-
unawaited object from their fixture that they will handle on their own. To suppress this warning
168-
when you in fact did intend to handle this you can wrap your async fixture in a synchronous fixture:
169-
170-
.. code-block:: python
171-
172-
import asyncio
173-
import pytest
174-
175-
176-
@pytest.fixture
177-
async def unawaited_fixture():
178-
return 1
179-
180-
181-
def test_foo(unawaited_fixture):
182-
assert 1 == asyncio.run(unawaited_fixture)
183-
184-
should be changed to
185-
186-
187-
.. code-block:: python
188-
189-
import asyncio
190-
import pytest
191-
192-
193-
@pytest.fixture
194-
def unawaited_fixture():
195-
async def inner_fixture():
196-
return 1
197-
198-
return inner_fixture()
199-
200-
201-
def test_foo(unawaited_fixture):
202-
assert 1 == asyncio.run(unawaited_fixture)
203-
204-
205-
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.
206-
207-
If a user has an async fixture with ``autouse=True`` in their ``conftest.py``, or in a file
208-
containing both synchronous tests and the fixture, they will receive this warning.
209-
Unless you're using a plugin that specifically handles async fixtures
210-
with synchronous tests, we strongly recommend against this practice.
211-
It can lead to unpredictable behavior (with larger scopes, it may appear to "work" if an async
212-
test is the first to request the fixture, due to value caching) and will generate
213-
unawaited-coroutine runtime warnings (but only for non-yield fixtures).
214-
Additionally, it creates ambiguity for other developers about whether the fixture is intended to perform
215-
setup for synchronous tests.
216-
217-
The `anyio pytest plugin <https://anyio.readthedocs.io/en/stable/testing.html>`_ supports
218-
synchronous tests with async fixtures, though certain limitations apply.
219-
220-
221151
.. _import-or-skip-import-error:
222152

223153
``pytest.importorskip`` default behavior regarding :class:`ImportError`
@@ -423,6 +353,78 @@ an appropriate period of deprecation has passed.
423353

424354
Some breaking changes which could not be deprecated are also listed.
425355

356+
.. _sync-test-async-fixture:
357+
358+
sync test depending on async fixture
359+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
360+
361+
.. deprecated:: 8.4
362+
.. versionremoved:: 9.0
363+
364+
Pytest has for a long time given an error when encountering an asynchronous test function, prompting the user to install
365+
a plugin that can handle it. It has not given any errors if you have an asynchronous fixture that's depended on by a
366+
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.
367+
This is a problem even if you do have a plugin installed for handling async tests, as they may require
368+
special decorators for async fixtures to be handled, and some may not robustly handle if a user accidentally requests an
369+
async fixture from their sync tests. Fixture values being cached can make this even more unintuitive, where everything will
370+
"work" if the fixture is first requested by an async test, and then requested by a synchronous test.
371+
372+
Unfortunately there is no 100% reliable method of identifying when a user has made a mistake, versus when they expect an
373+
unawaited object from their fixture that they will handle on their own. To suppress this warning
374+
when you in fact did intend to handle this you can wrap your async fixture in a synchronous fixture:
375+
376+
.. code-block:: python
377+
378+
import asyncio
379+
import pytest
380+
381+
382+
@pytest.fixture
383+
async def unawaited_fixture():
384+
return 1
385+
386+
387+
def test_foo(unawaited_fixture):
388+
assert 1 == asyncio.run(unawaited_fixture)
389+
390+
should be changed to
391+
392+
393+
.. code-block:: python
394+
395+
import asyncio
396+
import pytest
397+
398+
399+
@pytest.fixture
400+
def unawaited_fixture():
401+
async def inner_fixture():
402+
return 1
403+
404+
return inner_fixture()
405+
406+
407+
def test_foo(unawaited_fixture):
408+
assert 1 == asyncio.run(unawaited_fixture)
409+
410+
411+
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.
412+
413+
If a user has an async fixture with ``autouse=True`` in their ``conftest.py``, or in a file
414+
containing both synchronous tests and the fixture, they will receive this warning.
415+
Unless you're using a plugin that specifically handles async fixtures
416+
with synchronous tests, we strongly recommend against this practice.
417+
It can lead to unpredictable behavior (with larger scopes, it may appear to "work" if an async
418+
test is the first to request the fixture, due to value caching) and will generate
419+
unawaited-coroutine runtime warnings (but only for non-yield fixtures).
420+
Additionally, it creates ambiguity for other developers about whether the fixture is intended to perform
421+
setup for synchronous tests.
422+
423+
The `anyio pytest plugin <https://anyio.readthedocs.io/en/stable/testing.html>`_ supports
424+
synchronous tests with async fixtures, though certain limitations apply.
425+
426+
427+
426428
Applying a mark to a fixture function
427429
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
428430

src/_pytest/fixtures.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
from _pytest.scope import _ScopeName
6767
from _pytest.scope import HIGH_SCOPES
6868
from _pytest.scope import Scope
69-
from _pytest.warning_types import PytestRemovedIn9Warning
7069
from _pytest.warning_types import PytestWarning
7170

7271

@@ -1178,18 +1177,11 @@ def pytest_fixture_setup(
11781177
fixturefunc
11791178
):
11801179
auto_str = " with autouse=True" if fixturedef._autouse else ""
1181-
1182-
warnings.warn(
1183-
PytestRemovedIn9Warning(
1184-
f"{request.node.name!r} requested an async fixture "
1185-
f"{request.fixturename!r}{auto_str}, with no plugin or hook that "
1186-
"handled it. This is usually an error, as pytest does not natively "
1187-
"support it. "
1188-
"This will turn into an error in pytest 9.\n"
1189-
"See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture"
1190-
),
1191-
# no stacklevel will point at users code, so we just point here
1192-
stacklevel=1,
1180+
fail(
1181+
f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, "
1182+
"with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n"
1183+
"See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1184+
pytrace=False,
11931185
)
11941186

11951187
try:

testing/acceptance_test.py

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,7 +1307,7 @@ def test_3():
13071307
result.assert_outcomes(failed=3)
13081308

13091309

1310-
def test_warning_on_sync_test_async_fixture(pytester: Pytester) -> None:
1310+
def test_error_on_sync_test_async_fixture(pytester: Pytester) -> None:
13111311
pytester.makepyfile(
13121312
test_sync="""
13131313
import pytest
@@ -1324,23 +1324,17 @@ def test_foo(async_fixture):
13241324
pass
13251325
"""
13261326
)
1327-
result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning")
1327+
result = pytester.runpytest()
1328+
result.assert_outcomes(errors=1)
13281329
result.stdout.fnmatch_lines(
13291330
[
1330-
"*== warnings summary ==*",
1331-
(
1332-
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1333-
"fixture 'async_fixture', with no plugin or hook that handled it. "
1334-
"This is usually an error, as pytest does not natively support it. "
1335-
"This will turn into an error in pytest 9."
1336-
),
1337-
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1331+
"'test_foo' requested an async fixture 'async_fixture', with no plugin or hook that handled it. "
1332+
"This is an error, as pytest does not natively support it."
13381333
]
13391334
)
1340-
result.assert_outcomes(passed=1, warnings=1)
13411335

13421336

1343-
def test_warning_on_sync_test_async_fixture_gen(pytester: Pytester) -> None:
1337+
def test_error_on_sync_test_async_fixture_gen(pytester: Pytester) -> None:
13441338
pytester.makepyfile(
13451339
test_sync="""
13461340
import pytest
@@ -1354,23 +1348,17 @@ def test_foo(async_fixture):
13541348
...
13551349
"""
13561350
)
1357-
result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning")
1351+
result = pytester.runpytest()
1352+
result.assert_outcomes(errors=1)
13581353
result.stdout.fnmatch_lines(
13591354
[
1360-
"*== warnings summary ==*",
1361-
(
1362-
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1363-
"fixture 'async_fixture', with no plugin or hook that handled it. "
1364-
"This is usually an error, as pytest does not natively support it. "
1365-
"This will turn into an error in pytest 9."
1366-
),
1367-
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1355+
"'test_foo' requested an async fixture 'async_fixture', with no plugin or hook that handled it. "
1356+
"This is an error, as pytest does not natively support it."
13681357
]
13691358
)
1370-
result.assert_outcomes(passed=1, warnings=1)
13711359

13721360

1373-
def test_warning_on_sync_test_async_autouse_fixture(pytester: Pytester) -> None:
1361+
def test_error_on_sync_test_async_autouse_fixture(pytester: Pytester) -> None:
13741362
pytester.makepyfile(
13751363
test_sync="""
13761364
import pytest
@@ -1388,21 +1376,15 @@ def test_foo(async_fixture):
13881376
pass
13891377
"""
13901378
)
1391-
result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn9Warning")
1379+
result = pytester.runpytest()
1380+
result.assert_outcomes(errors=1)
13921381
result.stdout.fnmatch_lines(
13931382
[
1394-
"*== warnings summary ==*",
1395-
(
1396-
"*PytestRemovedIn9Warning: 'test_foo' requested an async "
1397-
"fixture 'async_fixture' with autouse=True, with no plugin or hook "
1398-
"that handled it. "
1399-
"This is usually an error, as pytest does not natively support it. "
1400-
"This will turn into an error in pytest 9."
1401-
),
1402-
" See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
1383+
"'test_foo' requested an async fixture 'async_fixture' with autouse=True, "
1384+
"with no plugin or hook that handled it. "
1385+
"This is an error, as pytest does not natively support it."
14031386
]
14041387
)
1405-
result.assert_outcomes(passed=1, warnings=1)
14061388

14071389

14081390
def test_pdb_can_be_rewritten(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)