From c790f7475e33be7790c63c18001e66577e69d58e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 30 Nov 2015 16:13:15 +0100 Subject: [PATCH 1/6] Fix getting line number with nasty __getattr__. When an object has a custom __getattr__ which always returns a non-int, we tried to get compat_co_firstlineno from it and checked it was a integer, which caused an exception if such a class is mistakenly collected. If we still mistakenly collect such a class (which is likely to be something other than a test), we now skip it with a warning (because it probably has an __init__) instead of producing an error. See #1204. --- CHANGELOG | 3 +++ _pytest/python.py | 5 +++-- testing/python/collect.py | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ae766aa0d..3299513d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,9 @@ module. Thanks Mikhail Chernykh for the report and Bruno Oliveira for the PR. +- fix #1204: another error when collecting with a nasty __getattr__(). + Thanks Florian Bruhin for the PR. + 2.8.3 ----- diff --git a/_pytest/python.py b/_pytest/python.py index c3a951772..67d5e5c52 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -384,12 +384,13 @@ class PyobjMixin(PyobjContext): def reportinfo(self): # XXX caching? obj = self.obj - if hasattr(obj, 'compat_co_firstlineno'): + compat_co_firstlineno = getattr(obj, 'compat_co_firstlineno', None) + if isinstance(compat_co_firstlineno, int): # nose compatibility fspath = sys.modules[obj.__module__].__file__ if fspath.endswith(".pyc"): fspath = fspath[:-1] - lineno = obj.compat_co_firstlineno + lineno = compat_co_firstlineno else: fspath, lineno = getfslineno(obj) modpath = self.getmodpath() diff --git a/testing/python/collect.py b/testing/python/collect.py index 636f9597e..1a85faf9c 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -880,6 +880,21 @@ class TestReportInfo: pass """ + def test_reportinfo_with_nasty_getattr(self, testdir): + # https://github.com/pytest-dev/pytest/issues/1204 + modcol = testdir.getmodulecol(""" + # lineno 0 + class TestClass: + def __getattr__(self, name): + return "this is not an int" + + def test_foo(self): + pass + """) + classcol = testdir.collect_by_name(modcol, "TestClass") + instance = classcol.collect()[0] + fspath, lineno, msg = instance.reportinfo() + def test_customized_python_discovery(testdir): testdir.makeini(""" From ba9146c131cc8a2e7d7a32dae59265a29ffa1061 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 30 Nov 2015 16:41:13 +0100 Subject: [PATCH 2/6] Don't collect classes with truthy __getattr__. When we have a metaclass which returns something truthy (like a method) in its __getattr__, we collected the class because pytest thought its __test__ attribute was set to True. We can work around this to some degree by assuming __test__ will always be set to an explicit True if that's what the user has intended, and if it's something other than that, this is probably a mistake. Fixes #1204. --- _pytest/python.py | 5 ++++- testing/python/integration.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/_pytest/python.py b/_pytest/python.py index c3a951772..9e7f4fb71 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -405,7 +405,10 @@ class PyCollector(PyobjMixin, pytest.Collector): """ Look for the __test__ attribute, which is applied by the @nose.tools.istest decorator """ - return safe_getattr(obj, '__test__', False) + # We explicitly check for "is True" here to not mistakenly treat + # classes with a custom __getattr__ returning something truthy (like a + # function) as test classes. + return safe_getattr(obj, '__test__', False) is True def classnamefilter(self, name): return self._matches_prefix_or_glob_option('python_classes', name) diff --git a/testing/python/integration.py b/testing/python/integration.py index 1b9be5968..0c436e32b 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -283,6 +283,35 @@ class TestNoselikeTestAttribute: assert len(call.items) == 1 assert call.items[0].cls.__name__ == "TC" + def test_class_with_nasty_getattr(self, testdir): + """Make sure we handle classes with a custom nasty __getattr__ right. + + With a custom __getattr__ which e.g. returns a function (like with a + RPC wrapper), we shouldn't assume this meant "__test__ = True". + """ + # https://github.com/pytest-dev/pytest/issues/1204 + testdir.makepyfile(""" + class MetaModel(type): + + def __getattr__(cls, key): + return lambda: None + + + BaseModel = MetaModel('Model', (), {}) + + + class Model(BaseModel): + + __metaclass__ = MetaModel + + def test_blah(self): + pass + """) + reprec = testdir.inline_run() + assert not reprec.getfailedcollections() + call = reprec.getcalls("pytest_collection_modifyitems")[0] + assert not call.items + @pytest.mark.issue351 class TestParameterize: From aba55a0fb23f1a5321988cd01f9f13a419105183 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 30 Nov 2015 16:59:12 +0100 Subject: [PATCH 3/6] Fix terminal output if no tests were run. Before: ==== in 0.00 seconds ==== After: ==== no tests run in 0.00 seconds ==== --- CHANGELOG | 3 +++ _pytest/terminal.py | 6 +++++- testing/test_terminal.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3299513d8..d57c16d06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,9 @@ - fix #1204: another error when collecting with a nasty __getattr__(). Thanks Florian Bruhin for the PR. +- fix the summary printed when no tests did run. + Thanks Florian Bruhin for the PR. + 2.8.3 ----- diff --git a/_pytest/terminal.py b/_pytest/terminal.py index efc8acc63..8a572e395 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -544,7 +544,11 @@ def build_summary_stats_line(stats): if val: key_name = key_translation.get(key, key) parts.append("%d %s" % (len(val), key_name)) - line = ", ".join(parts) + + if parts: + line = ", ".join(parts) + else: + line = "no tests run" if 'failed' in stats or 'error' in stats: color = 'red' diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cf0554077..0035bbeb0 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -779,10 +779,10 @@ def test_terminal_summary(testdir): ("green", "1 passed, 1 xpassed", {"xpassed": (1,), "passed": (1,)}), # Likewise if no tests were found at all - ("yellow", "", {}), + ("yellow", "no tests run", {}), # Test the empty-key special case - ("yellow", "", {"": (1,)}), + ("yellow", "no tests run", {"": (1,)}), ("green", "1 passed", {"": (1,), "passed": (1,)}), From 0d2668017dcf2ac618dfaeff671c24b603b5e0ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 30 Nov 2015 17:32:20 +0100 Subject: [PATCH 4/6] Fix spelling mistake in #1207. --- _pytest/terminal.py | 2 +- testing/test_terminal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 8a572e395..8aca7dd92 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -548,7 +548,7 @@ def build_summary_stats_line(stats): if parts: line = ", ".join(parts) else: - line = "no tests run" + line = "no tests ran" if 'failed' in stats or 'error' in stats: color = 'red' diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0035bbeb0..7c4b3eba6 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -779,10 +779,10 @@ def test_terminal_summary(testdir): ("green", "1 passed, 1 xpassed", {"xpassed": (1,), "passed": (1,)}), # Likewise if no tests were found at all - ("yellow", "no tests run", {}), + ("yellow", "no tests ran", {}), # Test the empty-key special case - ("yellow", "no tests run", {"": (1,)}), + ("yellow", "no tests ran", {"": (1,)}), ("green", "1 passed", {"": (1,), "passed": (1,)}), From 6be6798cdfedeffc0323375a9d095506f8c757ef Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 1 Dec 2015 01:41:47 -0800 Subject: [PATCH 5/6] Fix spelling: explicitely --> explicitly --- ISSUES.txt | 2 +- _pytest/runner.py | 2 +- _pytest/vendored_packages/pluggy.py | 2 +- doc/en/example/markers.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ISSUES.txt b/ISSUES.txt index b744a35f1..cc4b3ecc9 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -115,7 +115,7 @@ tags: feature - introduce pytest.mark.nocollect for not considering a function for test collection at all. maybe also introduce a pytest.mark.test to - explicitely mark a function to become a tested one. Lookup JUnit ways + explicitly mark a function to become a tested one. Lookup JUnit ways of tagging tests. introduce pytest.mark.importorskip diff --git a/_pytest/runner.py b/_pytest/runner.py index 6e4f45d5e..6d0f8b57b 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -469,7 +469,7 @@ def skip(msg=""): skip.Exception = Skipped def fail(msg="", pytrace=True): - """ explicitely fail an currently-executing test with the given Message. + """ explicitly fail an currently-executing test with the given Message. :arg pytrace: if false the msg represents the full failure information and no python traceback will be reported. diff --git a/_pytest/vendored_packages/pluggy.py b/_pytest/vendored_packages/pluggy.py index 2090dbb4e..2f848b23d 100644 --- a/_pytest/vendored_packages/pluggy.py +++ b/_pytest/vendored_packages/pluggy.py @@ -573,7 +573,7 @@ class _MultiCall: # XXX note that the __multicall__ argument is supported only # for pytest compatibility reasons. It was never officially - # supported there and is explicitely deprecated since 2.8 + # supported there and is explicitly deprecated since 2.8 # so we can remove it soon, allowing to avoid the below recursion # in execute() and simplify/speed up the execute loop. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 164f0abc1..789f41ac8 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -219,7 +219,7 @@ For an example on how to add and work with markers from a plugin, see .. note:: - It is recommended to explicitely register markers so that: + It is recommended to explicitly register markers so that: * there is one place in your test suite defining your markers From c7cf4adfd0b7098f599d65c0a261410d8ec8ca6f Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Tue, 1 Dec 2015 13:20:55 -0500 Subject: [PATCH 6/6] Update xdist link; BitBucket -> GitHub xdist is now hosted on GitHub. --- doc/en/writing_plugins.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 0baa9c16d..9df15079f 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -386,7 +386,7 @@ are expected. For an example, see `newhooks.py`_ from :ref:`xdist`. -.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default +.. _`newhooks.py`: https://github.com/pytest-dev/pytest-xdist/blob/974bd566c599dc6a9ea291838c6f226197208b46/xdist/newhooks.py Optionally using hooks from 3rd party plugins