From 2f1b192fe67eb4ce1c60ad61dc377ec8523293b6 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 12 Aug 2019 21:58:10 +0100 Subject: [PATCH] Issue a warning for async gen functions Co-Authored-By: Bruno Oliveira --- changelog/5734.bugfix.rst | 1 + src/_pytest/compat.py | 16 +++++++++------- src/_pytest/python.py | 10 ++++++---- testing/acceptance_test.py | 32 ++++++++++++++++++++++++++++++-- testing/test_compat.py | 26 +++++++++++++++++++++++--- 5 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 changelog/5734.bugfix.rst diff --git a/changelog/5734.bugfix.rst b/changelog/5734.bugfix.rst new file mode 100644 index 000000000..dc20e6b52 --- /dev/null +++ b/changelog/5734.bugfix.rst @@ -0,0 +1 @@ +Skip async generator test functions, and update the warning message to refer to ``async def`` functions. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 52ffc36bc..20be12d3e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -40,14 +40,16 @@ def is_generator(func): def iscoroutinefunction(func): - """Return True if func is a decorated coroutine function. - - Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly, - which in turns also initializes the "logging" module as side-effect (see issue #8). """ - return getattr(func, "_is_coroutine", False) or ( - hasattr(inspect, "iscoroutinefunction") and inspect.iscoroutinefunction(func) - ) + Return True if func is a coroutine function (a function defined with async + def syntax, and doesn't contain yield), or a function decorated with + @asyncio.coroutine. + + Note: copied and modified from Python 3.5's builtin couroutines.py to avoid + importing asyncio directly, which in turns also initializes the "logging" + module as a side-effect (see issue #8). + """ + return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) def getlocation(function, curdir): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 66d853060..70b486fb1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -23,6 +23,7 @@ from _pytest.compat import getfslineno from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator +from _pytest.compat import iscoroutinefunction from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -151,15 +152,16 @@ def pytest_configure(config): @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem): testfunction = pyfuncitem.obj - iscoroutinefunction = getattr(inspect, "iscoroutinefunction", None) - if iscoroutinefunction is not None and iscoroutinefunction(testfunction): - msg = "Coroutine functions are not natively supported and have been skipped.\n" + if iscoroutinefunction(testfunction) or ( + sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction) + ): + msg = "async def functions are not natively supported and have been skipped.\n" msg += "You need to install a suitable plugin for your async framework, for example:\n" msg += " - pytest-asyncio\n" msg += " - pytest-trio\n" msg += " - pytest-tornasync" warnings.warn(PytestUnhandledCoroutineWarning(msg.format(pyfuncitem.nodeid))) - skip(msg="coroutine function and no async plugin installed (see warnings)") + skip(msg="async def function and no async plugin installed (see warnings)") funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} testfunction(**testargs) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d2a348f40..55e3b966d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1199,11 +1199,39 @@ def test_warn_on_async_function(testdir): [ "test_async.py::test_1", "test_async.py::test_2", - "*Coroutine functions are not natively supported*", + "*async def functions are not natively supported*", "*2 skipped, 2 warnings in*", ] ) # ensure our warning message appears only once assert ( - result.stdout.str().count("Coroutine functions are not natively supported") == 1 + result.stdout.str().count("async def functions are not natively supported") == 1 + ) + + +@pytest.mark.filterwarnings("default") +@pytest.mark.skipif( + sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" +) +def test_warn_on_async_gen_function(testdir): + testdir.makepyfile( + test_async=""" + async def test_1(): + yield + async def test_2(): + yield + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "test_async.py::test_1", + "test_async.py::test_2", + "*async def functions are not natively supported*", + "*2 skipped, 2 warnings in*", + ] + ) + # ensure our warning message appears only once + assert ( + result.stdout.str().count("async def functions are not natively supported") == 1 ) diff --git a/testing/test_compat.py b/testing/test_compat.py index 9e7d05c5d..0244c07ac 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -91,9 +91,6 @@ def test_is_generator_asyncio(testdir): result.stdout.fnmatch_lines(["*1 passed*"]) -@pytest.mark.skipif( - sys.version_info < (3, 5), reason="async syntax available in Python 3.5+" -) def test_is_generator_async_syntax(testdir): testdir.makepyfile( """ @@ -113,6 +110,29 @@ def test_is_generator_async_syntax(testdir): result.stdout.fnmatch_lines(["*1 passed*"]) +@pytest.mark.skipif( + sys.version_info < (3, 6), reason="async gen syntax available in Python 3.6+" +) +def test_is_generator_async_gen_syntax(testdir): + testdir.makepyfile( + """ + from _pytest.compat import is_generator + def test_is_generator_py36(): + async def foo(): + yield + await foo() + + async def bar(): + yield + + assert not is_generator(foo) + assert not is_generator(bar) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + + class ErrorsHelper: @property def raise_exception(self):