python: collect: ignore exceptions with isinstance

Fixes https://github.com/pytest-dev/pytest/issues/4266.
This commit is contained in:
Daniel Hahler 2018-11-01 00:54:48 +01:00
parent 56e6bb0ff6
commit e30f7094f3
6 changed files with 74 additions and 1 deletions

1
doc/4266.bugfix.rst Normal file
View File

@ -0,0 +1 @@
Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class.

View File

@ -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.

View File

@ -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))

View File

@ -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 <class 'raises.ClassLooksIterableException'>"
):
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]

View File

@ -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*"])

View File

@ -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