diff --git a/doc/4266.bugfix.rst b/doc/4266.bugfix.rst new file mode 100644 index 000000000..f19a7cc1f --- /dev/null +++ b/doc/4266.bugfix.rst @@ -0,0 +1 @@ +Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4af0a2339..2b2cf659f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -334,6 +334,14 @@ def safe_getattr(object, name, default): return default +def safe_isclass(obj): + """Ignore any exception via isinstance on Python 3.""" + try: + return isclass(obj) + except Exception: + return False + + def _is_unittest_unexpected_success_a_failure(): """Return if the test suite should fail if an @expectedFailure unittest test PASSES. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 58f95034d..ec186bde1 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -33,6 +33,7 @@ from _pytest.compat import NoneType from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass from _pytest.compat import safe_str from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl @@ -195,7 +196,7 @@ def pytest_pycollect_makeitem(collector, name, obj): if res is not None: return # nothing was collected elsewhere, let's do it here - if isclass(obj): + if safe_isclass(obj): if collector.istestclass(obj, name): Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) diff --git a/testing/python/raises.py b/testing/python/raises.py index 9b9eadf1a..a72aeef63 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -1,5 +1,7 @@ import sys +import six + import pytest from _pytest.outcomes import Failed @@ -170,3 +172,25 @@ class TestRaises(object): Failed, match="DID NOT RAISE " ): pytest.raises(ClassLooksIterableException, lambda: None) + + def test_raises_with_raising_dunder_class(self): + """Test current behavior with regard to exceptions via __class__ (#4284).""" + + class CrappyClass(Exception): + @property + def __class__(self): + assert False, "via __class__" + + if six.PY2: + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.raises(CrappyClass()): + pass + assert "DID NOT RAISE" in excinfo.value.args[0] + + with pytest.raises(CrappyClass) as excinfo: + raise CrappyClass() + else: + with pytest.raises(AssertionError) as excinfo: + with pytest.raises(CrappyClass()): + pass + assert "via __class__" in excinfo.value.args[0] diff --git a/testing/test_collection.py b/testing/test_collection.py index 7f6791dae..751f484aa 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -977,3 +977,30 @@ def test_collect_invalid_signature_message(testdir): result.stdout.fnmatch_lines( ["Could not determine arguments of *.fix *: invalid method signature"] ) + + +def test_collect_handles_raising_on_dunder_class(testdir): + """Handle proxy classes like Django's LazySettings that might raise on + ``isinstance`` (#4266). + """ + testdir.makepyfile( + """ + class ImproperlyConfigured(Exception): + pass + + class RaisesOnGetAttr(object): + def raises(self): + raise ImproperlyConfigured + + __class__ = property(raises) + + raises = RaisesOnGetAttr() + + + def test_1(): + pass + """ + ) + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) diff --git a/testing/test_compat.py b/testing/test_compat.py index 494acd738..79224deef 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -12,6 +12,7 @@ from _pytest.compat import _PytestWrapper from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass from _pytest.outcomes import OutcomeException @@ -140,3 +141,14 @@ def test_safe_getattr(): helper = ErrorsHelper() assert safe_getattr(helper, "raise_exception", "default") == "default" assert safe_getattr(helper, "raise_fail", "default") == "default" + + +def test_safe_isclass(): + assert safe_isclass(type) is True + + class CrappyClass(Exception): + @property + def __class__(self): + assert False, "Should be ignored" + + assert safe_isclass(CrappyClass()) is False