diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6ad73d0c2..fff42ce3b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,8 @@ * -* +* pytest no longer recognizes coroutine functions as yield tests (`#2129`_). + Thanks to `@malinoff`_ for the PR. * Improve error message when pytest.warns fails (`#2150`_). The type(s) of the expected warnings and the list of caught warnings is added to the @@ -21,6 +22,9 @@ .. _#2150: https://github.com/pytest-dev/pytest/issues/2150 .. _#2148: https://github.com/pytest-dev/pytest/issues/2148 +.. _@malinoff: https://github.com/malinoff + +.. _#2129: https://github.com/pytest-dev/pytest/issues/2129 3.0.5 (2016-12-05) ================== diff --git a/_pytest/compat.py b/_pytest/compat.py index 51fc3bc5c..dc3e69545 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -19,6 +19,7 @@ except ImportError: # pragma: no cover # Only available in Python 3.4+ or as a backport enum = None + _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 @@ -42,11 +43,18 @@ REGEX_TYPE = type(re.compile('')) def is_generator(func): - try: - return _pytest._code.getrawcode(func).co_flags & 32 # generator function - except AttributeError: # builtin functions have no bytecode - # assume them to not be generators - return False + genfunc = inspect.isgeneratorfunction(func) + return genfunc and not iscoroutinefunction(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))) def getlocation(function, curdir): diff --git a/testing/test_compat.py b/testing/test_compat.py new file mode 100644 index 000000000..1fdd07e29 --- /dev/null +++ b/testing/test_compat.py @@ -0,0 +1,50 @@ +import sys + +import pytest +from _pytest.compat import is_generator + + +def test_is_generator(): + def zap(): + yield + + def foo(): + pass + + assert is_generator(zap) + assert not is_generator(foo) + + +@pytest.mark.skipif(sys.version_info < (3, 4), reason='asyncio available in Python 3.4+') +def test_is_generator_asyncio(testdir): + testdir.makepyfile(""" + from _pytest.compat import is_generator + import asyncio + @asyncio.coroutine + def baz(): + yield from [1,2,3] + + def test_is_generator_asyncio(): + assert not is_generator(baz) + """) + # avoid importing asyncio into pytest's own process, which in turn imports logging (#8) + result = testdir.runpytest_subprocess() + 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(""" + from _pytest.compat import is_generator + def test_is_generator_py35(): + async def foo(): + await foo() + + async def bar(): + pass + + assert not is_generator(foo) + assert not is_generator(bar) + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['*1 passed*'])