Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise a warning if @pytest.mark.asyncio is applied to non-async function #275

Merged
merged 7 commits into from Jan 21, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.rst
Expand Up @@ -257,6 +257,11 @@ or an async framework such as `asynctest <https://asynctest.readthedocs.io/en/la
Changelog
---------

0.18.0 (Unreleased)
~~~~~~~~~~~~~~~~~~~

- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 <https://github.com/pytest-dev/pytest-asyncio/issues/275>`_

0.17.2 (22-01-17)
~~~~~~~~~~~~~~~~~~~

Expand Down
66 changes: 42 additions & 24 deletions pytest_asyncio/plugin.py
Expand Up @@ -213,7 +213,8 @@ def pytest_pycollect_makeitem(
and _hypothesis_test_wraps_coroutine(obj)
):
item = pytest.Function.from_parent(collector, name=name)
if "asyncio" in item.keywords:
marker = item.get_closest_marker("asyncio")
if marker is not None:
return list(collector._genfunctions(name, obj))
else:
if _get_asyncio_mode(item.config) == Mode.AUTO:
Expand Down Expand Up @@ -390,16 +391,19 @@ def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Optional[object]:
Wraps marked tests in a synchronous function
where the wrapped test coroutine is executed in an event loop.
"""
if "asyncio" in pyfuncitem.keywords:
marker = pyfuncitem.get_closest_marker("asyncio")
if marker is not None:
funcargs: Dict[str, object] = pyfuncitem.funcargs # type: ignore[name-defined]
loop = cast(asyncio.AbstractEventLoop, funcargs["event_loop"])
if _is_hypothesis_test(pyfuncitem.obj):
pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
pyfuncitem,
pyfuncitem.obj.hypothesis.inner_test,
_loop=loop,
)
else:
pyfuncitem.obj = wrap_in_sync(
pyfuncitem,
pyfuncitem.obj,
_loop=loop,
)
Expand All @@ -410,7 +414,11 @@ def _is_hypothesis_test(function: Any) -> bool:
return getattr(function, "is_hypothesis_test", False)


def wrap_in_sync(func: Callable[..., Awaitable[Any]], _loop: asyncio.AbstractEventLoop):
def wrap_in_sync(
pyfuncitem: pytest.Function,
func: Callable[..., Awaitable[Any]],
_loop: asyncio.AbstractEventLoop,
):
"""Return a sync wrapper around an async function executing it in the
current event loop."""

Expand All @@ -424,34 +432,44 @@ def wrap_in_sync(func: Callable[..., Awaitable[Any]], _loop: asyncio.AbstractEve
@functools.wraps(func)
def inner(**kwargs):
coro = func(**kwargs)
if coro is not None:
task = asyncio.ensure_future(coro, loop=_loop)
try:
_loop.run_until_complete(task)
except BaseException:
# run_until_complete doesn't get the result from exceptions
# that are not subclasses of `Exception`. Consume all
# exceptions to prevent asyncio's warning from logging.
if task.done() and not task.cancelled():
task.exception()
raise
if not inspect.isawaitable(coro):
pyfuncitem.warn(
pytest.PytestWarning(
f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' "
"but it is not an async function. "
"Please remove asyncio marker. "
"If the test is not marked explicitly, "
"check for global markers applied via 'pytestmark'."
)
)
return
task = asyncio.ensure_future(coro, loop=_loop)
try:
_loop.run_until_complete(task)
except BaseException:
# run_until_complete doesn't get the result from exceptions
# that are not subclasses of `Exception`. Consume all
# exceptions to prevent asyncio's warning from logging.
if task.done() and not task.cancelled():
task.exception()
raise

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


def pytest_runtest_setup(item: pytest.Item) -> None:
if "asyncio" in item.keywords:
fixturenames = item.fixturenames # type: ignore[attr-defined]
# inject an event loop fixture for all async tests
if "event_loop" in fixturenames:
fixturenames.remove("event_loop")
fixturenames.insert(0, "event_loop")
marker = item.get_closest_marker("asyncio")
if marker is None:
return
fixturenames = item.fixturenames # type: ignore[attr-defined]
# inject an event loop fixture for all async tests
if "event_loop" in fixturenames:
fixturenames.remove("event_loop")
fixturenames.insert(0, "event_loop")
obj = getattr(item, "obj", None)
if (
item.get_closest_marker("asyncio") is not None
and not getattr(obj, "hypothesis", False)
and getattr(obj, "is_hypothesis_test", False)
if not getattr(obj, "hypothesis", False) and getattr(
obj, "is_hypothesis_test", False
):
pytest.fail(
"test function `%r` is using Hypothesis, but pytest-asyncio "
Expand Down
39 changes: 36 additions & 3 deletions tests/test_simple.py
@@ -1,5 +1,6 @@
"""Quick'n'dirty unit tests for provided fixtures and markers."""
import asyncio
from textwrap import dedent

import pytest

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

@pytest.mark.xfail(reason="need a failure", strict=True)
@pytest.mark.asyncio
def test_asyncio_marker_fail():
async def test_asyncio_marker_fail():
raise AssertionError


@pytest.mark.asyncio
def test_asyncio_marker_with_default_param(a_param=None):
async def test_asyncio_marker_with_default_param(a_param=None):
"""Test the asyncio pytest marker."""
yield # sleep(0)
await asyncio.sleep(0)


@pytest.mark.asyncio
Expand Down Expand Up @@ -240,3 +241,35 @@ async def test_no_warning_on_skip():
def test_async_close_loop(event_loop):
event_loop.close()
return "ok"


def test_warn_asyncio_marker_for_regular_func(testdir):
testdir.makepyfile(
dedent(
"""\
import pytest

pytest_plugins = 'pytest_asyncio'

@pytest.mark.asyncio
def test_a():
pass
"""
)
)
testdir.makefile(
".ini",
pytest=dedent(
"""\
[pytest]
asyncio_mode = strict
filterwarnings =
default
"""
),
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)
result.stdout.fnmatch_lines(
["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"]
)