From 3a59acf69f0121302bcddd34f71fcd74b966ce7c Mon Sep 17 00:00:00 2001 From: Dmitry Malinovsky Date: Sun, 11 Dec 2016 21:59:11 +0600 Subject: [PATCH 1/4] Use inspect to properly detect generators. Fixes #2129 --- _pytest/compat.py | 15 ++++++++++----- testing/conftest.py | 7 +++++++ testing/test_compat.py | 12 ++++++++++++ testing/test_compat_3.py | 15 +++++++++++++++ testing/test_compat_35.py | 12 ++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 testing/conftest.py create mode 100644 testing/test_compat.py create mode 100644 testing/test_compat_3.py create mode 100644 testing/test_compat_35.py diff --git a/_pytest/compat.py b/_pytest/compat.py index 51fc3bc5c1b..7a3d0af8173 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -19,6 +19,12 @@ # Only available in Python 3.4+ or as a backport enum = None +try: + import asyncio +except ImportError: # pragma: no cover + # Only available in Python 3.4+ or as a backport + asyncio = None + _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 @@ -42,11 +48,10 @@ def _format_args(func): 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) + if asyncio is not None: + return genfunc and not asyncio.iscoroutinefunction(func) + return genfunc def getlocation(function, curdir): diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 00000000000..76a314837e0 --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,7 @@ +import sys + +collect_ignore = [] +if sys.version_info[0] < 3: + collect_ignore.append("test_compat_3.py") +if sys.version_info < (3, 5): + collect_ignore.append("test_compat_35.py") diff --git a/testing/test_compat.py b/testing/test_compat.py new file mode 100644 index 00000000000..185fc3bd689 --- /dev/null +++ b/testing/test_compat.py @@ -0,0 +1,12 @@ +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) diff --git a/testing/test_compat_3.py b/testing/test_compat_3.py new file mode 100644 index 00000000000..a1ee0e6f122 --- /dev/null +++ b/testing/test_compat_3.py @@ -0,0 +1,15 @@ +import pytest +from _pytest.compat import is_generator +try: + import asyncio +except ImportError: + asyncio = None + + +@pytest.mark.skipif(asyncio is None, reason='asyncio is not installed') +def test_is_generator(): + @asyncio.coroutine + def baz(): + yield from [1,2,3] + + assert not is_generator(baz) diff --git a/testing/test_compat_35.py b/testing/test_compat_35.py new file mode 100644 index 00000000000..93eecc920a6 --- /dev/null +++ b/testing/test_compat_35.py @@ -0,0 +1,12 @@ +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) From 45eb9b566c318b529219263936a8592cb4c26a88 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Dec 2016 21:28:07 -0200 Subject: [PATCH 2/4] Move compat tests to a single file using testdir This avoids having to resort to skipping modules in conftest.py file and avoids flake8 errors --- testing/conftest.py | 7 ------- testing/test_compat.py | 37 +++++++++++++++++++++++++++++++++++++ testing/test_compat_3.py | 15 --------------- testing/test_compat_35.py | 12 ------------ 4 files changed, 37 insertions(+), 34 deletions(-) delete mode 100644 testing/conftest.py delete mode 100644 testing/test_compat_3.py delete mode 100644 testing/test_compat_35.py diff --git a/testing/conftest.py b/testing/conftest.py deleted file mode 100644 index 76a314837e0..00000000000 --- a/testing/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -import sys - -collect_ignore = [] -if sys.version_info[0] < 3: - collect_ignore.append("test_compat_3.py") -if sys.version_info < (3, 5): - collect_ignore.append("test_compat_35.py") diff --git a/testing/test_compat.py b/testing/test_compat.py index 185fc3bd689..d704bb80711 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,3 +1,6 @@ +import sys + +import pytest from _pytest.compat import is_generator @@ -10,3 +13,37 @@ def foo(): assert is_generator(zap) assert not is_generator(foo) + + +def test_is_generator_asyncio(testdir): + pytest.importorskip('asyncio') + 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) + """) + result = testdir.runpytest() + 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*']) diff --git a/testing/test_compat_3.py b/testing/test_compat_3.py deleted file mode 100644 index a1ee0e6f122..00000000000 --- a/testing/test_compat_3.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -from _pytest.compat import is_generator -try: - import asyncio -except ImportError: - asyncio = None - - -@pytest.mark.skipif(asyncio is None, reason='asyncio is not installed') -def test_is_generator(): - @asyncio.coroutine - def baz(): - yield from [1,2,3] - - assert not is_generator(baz) diff --git a/testing/test_compat_35.py b/testing/test_compat_35.py deleted file mode 100644 index 93eecc920a6..00000000000 --- a/testing/test_compat_35.py +++ /dev/null @@ -1,12 +0,0 @@ -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) From 1312b83866709611aaa9d9d22e3161dfd9b8d244 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Dec 2016 21:33:01 -0200 Subject: [PATCH 3/4] Add CHANGELOG entry for #2129 --- CHANGELOG.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70725395888..3bb525c1e9c 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. * @@ -12,6 +13,9 @@ * +.. _@malinoff: https://github.com/malinoff + +.. _#2129: https://github.com/pytest-dev/pytest/issues/2129 3.0.5 (2016-12-05) ================== From caee5ce489e4f7058154bea1b4bc371bd384af20 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Dec 2016 21:54:20 -0200 Subject: [PATCH 4/4] Avoid importing asyncio directly because that in turn initializes logging (#8) --- _pytest/compat.py | 19 +++++++++++-------- testing/test_compat.py | 5 +++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 7a3d0af8173..dc3e695454f 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -19,11 +19,6 @@ # Only available in Python 3.4+ or as a backport enum = None -try: - import asyncio -except ImportError: # pragma: no cover - # Only available in Python 3.4+ or as a backport - asyncio = None _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 @@ -49,9 +44,17 @@ def _format_args(func): def is_generator(func): genfunc = inspect.isgeneratorfunction(func) - if asyncio is not None: - return genfunc and not asyncio.iscoroutinefunction(func) - return genfunc + 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 index d704bb80711..1fdd07e29c6 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -15,8 +15,8 @@ def foo(): 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): - pytest.importorskip('asyncio') testdir.makepyfile(""" from _pytest.compat import is_generator import asyncio @@ -27,7 +27,8 @@ def baz(): def test_is_generator_asyncio(): assert not is_generator(baz) """) - result = testdir.runpytest() + # avoid importing asyncio into pytest's own process, which in turn imports logging (#8) + result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(['*1 passed*'])