diff --git a/CHANGELOG b/CHANGELOG index 288d9a6c6..e335e7a03 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,12 @@ - fix #1198: ``--pastebin`` option now works on Python 3. Thanks Mehdy Khoshnoody for the PR. +- 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/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/python.py b/_pytest/python.py index c3a951772..4ff3f4fc0 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() @@ -405,7 +406,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/_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/terminal.py b/_pytest/terminal.py index efc8acc63..8aca7dd92 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 ran" if 'failed' in stats or 'error' in stats: color = 'red' 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 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 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(""" 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: diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cf0554077..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", "", {}), + ("yellow", "no tests ran", {}), # Test the empty-key special case - ("yellow", "", {"": (1,)}), + ("yellow", "no tests ran", {"": (1,)}), ("green", "1 passed", {"": (1,), "passed": (1,)}),