From ba9146c131cc8a2e7d7a32dae59265a29ffa1061 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 30 Nov 2015 16:41:13 +0100 Subject: [PATCH] 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: