diff --git a/CHANGELOG b/CHANGELOG index ce75fced1..cec72c229 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 2.8.0.dev (compared to 2.7.X) ----------------------------- +- Fix #562: @nose.tools.istest now fully respected. + - parametrize now also generates meaningful test IDs for enum, regex and class objects (as opposed to class instances). Thanks to Florian Bruhin for the PR. diff --git a/_pytest/python.py b/_pytest/python.py index 68ffda308..8438ca428 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -37,7 +37,7 @@ def filter_traceback(entry): def get_real_func(obj): - """gets the real function object of the (possibly) wrapped object by + """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. """ while hasattr(obj, "__wrapped__"): @@ -64,6 +64,17 @@ def getimfunc(func): except AttributeError: return func +def safe_getattr(object, name, default): + """ Like getattr but return default upon any Exception. + + Attribute access can potentially fail for 'evil' Python objects. + See issue214 + """ + try: + return getattr(object, name, default) + except Exception: + return default + class FixtureFunctionMarker: def __init__(self, scope, params, @@ -257,11 +268,10 @@ def pytest_pycollect_makeitem(collector, name, obj): raise StopIteration # nothing was collected elsewhere, let's do it here if isclass(obj): - if collector.classnamefilter(name): + if collector.istestclass(obj, name): Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) - elif collector.funcnamefilter(name) and hasattr(obj, "__call__") and\ - getfixturemarker(obj) is None: + elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it obj = getattr(obj, "__func__", obj) if not isfunction(obj): @@ -347,9 +357,24 @@ class PyCollector(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): return self._matches_prefix_or_glob_option('python_functions', name) + def isnosetest(self, obj): + """ Look for the __test__ attribute, which is applied by the + @nose.tools.istest decorator + """ + return safe_getattr(obj, '__test__', False) + def classnamefilter(self, name): return self._matches_prefix_or_glob_option('python_classes', name) + def istestfunction(self, obj, name): + return ( + (self.funcnamefilter(name) or self.isnosetest(obj)) + and safe_getattr(obj, "__call__", False) and getfixturemarker(obj) is None + ) + + def istestclass(self, obj, name): + return self.classnamefilter(name) or self.isnosetest(obj) + def _matches_prefix_or_glob_option(self, option_name, name): """ checks if the given name matches the prefix or glob-pattern defined @@ -494,7 +519,7 @@ class FuncFixtureInfo: def _marked(func, mark): - """Returns True if :func: is already marked with :mark:, False orherwise. + """ Returns True if :func: is already marked with :mark:, False otherwise. This can happen if marker is applied to class and the test file is invoked more than once. """ @@ -1130,9 +1155,9 @@ def raises(expected_exception, *args, **kwargs): " derived from BaseException, not %s") if isinstance(expected_exception, tuple): for exc in expected_exception: - if not inspect.isclass(exc): + if not isclass(exc): raise TypeError(msg % type(exc)) - elif not inspect.isclass(expected_exception): + elif not isclass(expected_exception): raise TypeError(msg % type(expected_exception)) if not args: @@ -1379,7 +1404,7 @@ class FixtureRequest(FuncargnamesCompatAttr): return self._pyfuncitem.session def addfinalizer(self, finalizer): - """add finalizer/teardown function to be called after the + """ add finalizer/teardown function to be called after the last test within the requesting test context finished execution. """ # XXX usually this method is shadowed by fixturedef specific ones diff --git a/testing/test_nose.py b/testing/test_nose.py index 76873a834..6260aae47 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -347,3 +347,49 @@ def test_SkipTest_in_test(testdir): """) reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) + +def test_istest_function_decorator(testdir): + p = testdir.makepyfile(""" + import nose.tools + @nose.tools.istest + def not_test_prefix(): + pass + """) + result = testdir.runpytest(p) + result.assert_outcomes(passed=1) + +def test_nottest_function_decorator(testdir): + testdir.makepyfile(""" + import nose.tools + @nose.tools.nottest + def test_prefix(): + pass + """) + reprec = testdir.inline_run() + assert not reprec.getfailedcollections() + calls = reprec.getreports("pytest_runtest_logreport") + assert not calls + +def test_istest_class_decorator(testdir): + p = testdir.makepyfile(""" + import nose.tools + @nose.tools.istest + class NotTestPrefix: + def test_method(self): + pass + """) + result = testdir.runpytest(p) + result.assert_outcomes(passed=1) + +def test_nottest_class_decorator(testdir): + testdir.makepyfile(""" + import nose.tools + @nose.tools.nottest + class TestPrefix: + def test_method(self): + pass + """) + reprec = testdir.inline_run() + assert not reprec.getfailedcollections() + calls = reprec.getreports("pytest_runtest_logreport") + assert not calls