Skip to content

Commit dab3b51

Browse files
authored
Raise a warning if @pytest.mark.asyncio is applied to non-async function (#275)
1 parent 048a6ed commit dab3b51

File tree

3 files changed

+83
-27
lines changed

3 files changed

+83
-27
lines changed

README.rst

+5
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/la
257257
Changelog
258258
---------
259259

260+
0.18.0 (Unreleased)
261+
~~~~~~~~~~~~~~~~~~~
262+
263+
- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 <https://github.com/pytest-dev/pytest-asyncio/issues/275>`_
264+
260265
0.17.2 (22-01-17)
261266
~~~~~~~~~~~~~~~~~~~
262267

pytest_asyncio/plugin.py

+42-24
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ def pytest_pycollect_makeitem(
213213
and _hypothesis_test_wraps_coroutine(obj)
214214
):
215215
item = pytest.Function.from_parent(collector, name=name)
216-
if "asyncio" in item.keywords:
216+
marker = item.get_closest_marker("asyncio")
217+
if marker is not None:
217218
return list(collector._genfunctions(name, obj))
218219
else:
219220
if _get_asyncio_mode(item.config) == Mode.AUTO:
@@ -390,16 +391,19 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]:
390391
Wraps marked tests in a synchronous function
391392
where the wrapped test coroutine is executed in an event loop.
392393
"""
393-
if "asyncio" in pyfuncitem.keywords:
394+
marker = pyfuncitem.get_closest_marker("asyncio")
395+
if marker is not None:
394396
funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined]
395397
loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"])
396398
if _is_hypothesis_test(pyfuncitem.obj):
397399
pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
400+
pyfuncitem,
398401
pyfuncitem.obj.hypothesis.inner_test,
399402
_loop=loop,
400403
)
401404
else:
402405
pyfuncitem.obj = wrap_in_sync(
406+
pyfuncitem,
403407
pyfuncitem.obj,
404408
_loop=loop,
405409
)
@@ -410,7 +414,11 @@ def _is_hypothesis_test(function: Any) -> bool:
410414
return getattr(function, "is_hypothesis_test", False)
411415

412416

413-
def wrap_in_sync(func: Callable[..., Awaitable[Any]], _loop: asyncio.AbstractEventLoop):
417+
def wrap_in_sync(
418+
pyfuncitem: pytest.Function,
419+
func: Callable[..., Awaitable[Any]],
420+
_loop: asyncio.AbstractEventLoop,
421+
):
414422
"""Return a sync wrapper around an async function executing it in the
415423
current event loop."""
416424

@@ -424,34 +432,44 @@ def wrap_in_sync(func: Callable[..., Awaitable[Any]], _loop: asyncio.AbstractEve
424432
@functools.wraps(func)
425433
def inner(**kwargs):
426434
coro = func(**kwargs)
427-
if coro is not None:
428-
task = asyncio.ensure_future(coro, loop=_loop)
429-
try:
430-
_loop.run_until_complete(task)
431-
except BaseException:
432-
# run_until_complete doesn't get the result from exceptions
433-
# that are not subclasses of `Exception`. Consume all
434-
# exceptions to prevent asyncio's warning from logging.
435-
if task.done() and not task.cancelled():
436-
task.exception()
437-
raise
435+
if not inspect.isawaitable(coro):
436+
pyfuncitem.warn(
437+
pytest.PytestWarning(
438+
f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
439+
"but it is not an async function. "
440+
"Please remove asyncio marker. "
441+
"If the test is not marked explicitly, "
442+
"check for global markers applied via 'pytestmark'."
443+
)
444+
)
445+
return
446+
task = asyncio.ensure_future(coro, loop=_loop)
447+
try:
448+
_loop.run_until_complete(task)
449+
except BaseException:
450+
# run_until_complete doesn't get the result from exceptions
451+
# that are not subclasses of `Exception`. Consume all
452+
# exceptions to prevent asyncio's warning from logging.
453+
if task.done() and not task.cancelled():
454+
task.exception()
455+
raise
438456

439457
inner._raw_test_func = func # type: ignore[attr-defined]
440458
return inner
441459

442460

443461
def pytest_runtest_setup(item: pytest.Item) -> None:
444-
if "asyncio" in item.keywords:
445-
fixturenames = item.fixturenames # type: ignore[attr-defined]
446-
# inject an event loop fixture for all async tests
447-
if "event_loop" in fixturenames:
448-
fixturenames.remove("event_loop")
449-
fixturenames.insert(0, "event_loop")
462+
marker = item.get_closest_marker("asyncio")
463+
if marker is None:
464+
return
465+
fixturenames = item.fixturenames # type: ignore[attr-defined]
466+
# inject an event loop fixture for all async tests
467+
if "event_loop" in fixturenames:
468+
fixturenames.remove("event_loop")
469+
fixturenames.insert(0, "event_loop")
450470
obj = getattr(item, "obj", None)
451-
if (
452-
item.get_closest_marker("asyncio") is not None
453-
and not getattr(obj, "hypothesis", False)
454-
and getattr(obj, "is_hypothesis_test", False)
471+
if not getattr(obj, "hypothesis", False) and getattr(
472+
obj, "is_hypothesis_test", False
455473
):
456474
pytest.fail(
457475
"test function `%r` is using Hypothesis, but pytest-asyncio "

tests/test_simple.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Quick'n'dirty unit tests for provided fixtures and markers."""
22
import asyncio
3+
from textwrap import dedent
34

45
import pytest
56

@@ -26,14 +27,14 @@ async def test_asyncio_marker():
2627

2728
@pytest.mark.xfail(reason="need a failure", strict=True)
2829
@pytest.mark.asyncio
29-
def test_asyncio_marker_fail():
30+
async def test_asyncio_marker_fail():
3031
raise AssertionError
3132

3233

3334
@pytest.mark.asyncio
34-
def test_asyncio_marker_with_default_param(a_param=None):
35+
async def test_asyncio_marker_with_default_param(a_param=None):
3536
"""Test the asyncio pytest marker."""
36-
yield # sleep(0)
37+
await asyncio.sleep(0)
3738

3839

3940
@pytest.mark.asyncio
@@ -240,3 +241,35 @@ async def test_no_warning_on_skip():
240241
def test_async_close_loop(event_loop):
241242
event_loop.close()
242243
return "ok"
244+
245+
246+
def test_warn_asyncio_marker_for_regular_func(testdir):
247+
testdir.makepyfile(
248+
dedent(
249+
"""\
250+
import pytest
251+
252+
pytest_plugins = 'pytest_asyncio'
253+
254+
@pytest.mark.asyncio
255+
def test_a():
256+
pass
257+
"""
258+
)
259+
)
260+
testdir.makefile(
261+
".ini",
262+
pytest=dedent(
263+
"""\
264+
[pytest]
265+
asyncio_mode = strict
266+
filterwarnings =
267+
default
268+
"""
269+
),
270+
)
271+
result = testdir.runpytest()
272+
result.assert_outcomes(passed=1)
273+
result.stdout.fnmatch_lines(
274+
["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
275+
)

0 commit comments

Comments
 (0)