From 184514e946a8dd67d301b6d49046e715e4196459 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 15:20:26 +0200 Subject: [PATCH 1/7] Raise a warning if @pytest.mark.asyncio is applied to non-async function --- README.rst | 5 ++++ pytest_asyncio/plugin.py | 65 +++++++++++++++++++++++++--------------- tests/test_simple.py | 28 +++++++++++++++-- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/README.rst b/README.rst index 68df7e09..b5645dd1 100644 --- a/README.rst +++ b/README.rst @@ -257,6 +257,11 @@ or an async framework such as `asynctest 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, ) @@ -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.""" @@ -424,34 +432,43 @@ 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( + UserWarning( + f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " + "but it is not an async function. " + "Please remove asyncio marker, " + "use 'asyncio_mode = auto' for autocollection." + ) + ) + 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 " diff --git a/tests/test_simple.py b/tests/test_simple.py index 31204b6c..cc101152 100644 --- a/tests/test_simple.py +++ b/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 @@ -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 @@ -240,3 +241,24 @@ 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 + """ + ) + ) + result = testdir.runpytest_subprocess("--asyncio-mode=strict") + result.assert_outcomes(passed=1) + result.stdout.fnmatch_lines( + ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] + ) From c30cef66d92956bbc03003e2dee0c7d66006b0ef Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 15:21:35 +0200 Subject: [PATCH 2/7] Update README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b5645dd1..fc668bfa 100644 --- a/README.rst +++ b/README.rst @@ -260,7 +260,7 @@ Changelog 0.18.0 (Unreleased) ~~~~~~~~~~~~~~~~~~~ -- TODO. +- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 Date: Fri, 21 Jan 2022 15:36:50 +0200 Subject: [PATCH 3/7] Fix test on py3.7 and pytest 6.1.0 --- tests/test_simple.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_simple.py b/tests/test_simple.py index cc101152..2fc8656a 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -257,7 +257,18 @@ def test_a(): """ ) ) - result = testdir.runpytest_subprocess("--asyncio-mode=strict") + testdir.makefile( + ".ini", + pytest=dedent( + """\ + [pytest] + asyncio_mode = strict + filterwarnings = + default + """ + ), + ) + result = testdir.runpytest_subprocess() result.assert_outcomes(passed=1) result.stdout.fnmatch_lines( ["*is marked with '@pytest.mark.asyncio' but it is not an async function.*"] From 1dba69b8eba40b8222f2b5c257e9ad498d590ac3 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 15:38:22 +0200 Subject: [PATCH 4/7] Fix readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fc668bfa..2d00f5b9 100644 --- a/README.rst +++ b/README.rst @@ -260,7 +260,7 @@ Changelog 0.18.0 (Unreleased) ~~~~~~~~~~~~~~~~~~~ -- Raise a warning if @pytest.mark.asyncio is applied to non-async function. `#275 `_ 0.17.2 (22-01-17) ~~~~~~~~~~~~~~~~~~~ From e465284b877d1ef3b74b5b6ccd6fde63d956823b Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 15:43:01 +0200 Subject: [PATCH 5/7] Fix --- pytest_asyncio/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2243b34b..c5d7f761 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -434,7 +434,7 @@ def inner(**kwargs): coro = func(**kwargs) if not inspect.isawaitable(coro): pyfuncitem.warn( - UserWarning( + pytest.PytestWarning( f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " "but it is not an async function. " "Please remove asyncio marker, " From 7c5839e8e106097f9209e99561019c07158f9846 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 15:50:07 +0200 Subject: [PATCH 6/7] Use inplace pytester --- tests/test_simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_simple.py b/tests/test_simple.py index 2fc8656a..dc68d61e 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -268,7 +268,7 @@ def test_a(): """ ), ) - result = testdir.runpytest_subprocess() + 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.*"] From 8d644db9ec3c0494c9292ff638522c9dd5f0de47 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 21 Jan 2022 16:10:03 +0200 Subject: [PATCH 7/7] Fix message text --- pytest_asyncio/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index c5d7f761..8d9aa980 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -437,8 +437,9 @@ def inner(**kwargs): pytest.PytestWarning( f"The test {pyfuncitem} is marked with '@pytest.mark.asyncio' " "but it is not an async function. " - "Please remove asyncio marker, " - "use 'asyncio_mode = auto' for autocollection." + "Please remove asyncio marker. " + "If the test is not marked explicitly, " + "check for global markers applied via 'pytestmark'." ) ) return