Skip to content

Commit c9adfd1

Browse files
committed
python: collect: ignore exceptions with isinstance
Fixes #4266.
1 parent 56e6bb0 commit c9adfd1

File tree

6 files changed

+77
-1
lines changed

6 files changed

+77
-1
lines changed

doc/4266.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class.

src/_pytest/compat.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@ def safe_getattr(object, name, default):
334334
return default
335335

336336

337+
def safe_isclass(obj):
338+
"""Ignore any exception via isinstance on Python 3."""
339+
try:
340+
return isclass(obj)
341+
except Exception:
342+
return False
343+
344+
337345
def _is_unittest_unexpected_success_a_failure():
338346
"""Return if the test suite should fail if an @expectedFailure unittest test PASSES.
339347

src/_pytest/python.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from _pytest.compat import NOTSET
3434
from _pytest.compat import REGEX_TYPE
3535
from _pytest.compat import safe_getattr
36+
from _pytest.compat import safe_isclass
3637
from _pytest.compat import safe_str
3738
from _pytest.compat import STRING_TYPES
3839
from _pytest.config import hookimpl
@@ -195,7 +196,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
195196
if res is not None:
196197
return
197198
# nothing was collected elsewhere, let's do it here
198-
if isclass(obj):
199+
if safe_isclass(obj):
199200
if collector.istestclass(obj, name):
200201
Class = collector._getcustomclass("Class")
201202
outcome.force_result(Class(name, parent=collector))

testing/python/raises.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sys
22

3+
import six
4+
35
import pytest
46
from _pytest.outcomes import Failed
57

@@ -170,3 +172,28 @@ class ClassLooksIterableException(Exception):
170172
Failed, match="DID NOT RAISE <class 'raises.ClassLooksIterableException'>"
171173
):
172174
pytest.raises(ClassLooksIterableException, lambda: None)
175+
176+
def test_raises_with_raising_dunder_class(self):
177+
"""Test current behavior with regard to exceptions via __class__ (#4284)."""
178+
179+
class CrappyClass(Exception):
180+
@property
181+
def __class__(self):
182+
assert False, "via __class__"
183+
184+
def __call__(self):
185+
return CrappyClass
186+
187+
if six.PY2:
188+
with pytest.raises(pytest.fail.Exception) as excinfo:
189+
with pytest.raises(CrappyClass()):
190+
pass
191+
assert "DID NOT RAISE" in excinfo.value.args[0]
192+
193+
with pytest.raises(CrappyClass) as excinfo:
194+
raise CrappyClass()
195+
else:
196+
with pytest.raises(AssertionError) as excinfo:
197+
with pytest.raises(CrappyClass()):
198+
pass
199+
assert "via __class__" in excinfo.value.args[0]

testing/test_collection.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,3 +977,30 @@ def fix():
977977
result.stdout.fnmatch_lines(
978978
["Could not determine arguments of *.fix *: invalid method signature"]
979979
)
980+
981+
982+
def test_collect_handles_raising_on_dunder_class(testdir):
983+
"""Handle proxy classes like Django's LazySettings that might raise on
984+
``isinstance`` (#4266).
985+
"""
986+
testdir.makepyfile(
987+
"""
988+
class ImproperlyConfigured(Exception):
989+
pass
990+
991+
class RaisesOnGetAttr(object):
992+
def raises(self):
993+
raise ImproperlyConfigured
994+
995+
__class__ = property(raises)
996+
997+
raises = RaisesOnGetAttr()
998+
999+
1000+
def test_1():
1001+
pass
1002+
"""
1003+
)
1004+
result = testdir.runpytest()
1005+
assert result.ret == 0
1006+
result.stdout.fnmatch_lines(["*1 passed in*"])

testing/test_compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from _pytest.compat import get_real_func
1313
from _pytest.compat import is_generator
1414
from _pytest.compat import safe_getattr
15+
from _pytest.compat import safe_isclass
1516
from _pytest.outcomes import OutcomeException
1617

1718

@@ -140,3 +141,14 @@ def test_safe_getattr():
140141
helper = ErrorsHelper()
141142
assert safe_getattr(helper, "raise_exception", "default") == "default"
142143
assert safe_getattr(helper, "raise_fail", "default") == "default"
144+
145+
146+
def test_safe_isclass():
147+
assert safe_isclass(type) is True
148+
149+
class CrappyClass(Exception):
150+
@property
151+
def __class__(self):
152+
assert False, "Should be ignored"
153+
154+
assert safe_isclass(CrappyClass()) is False

0 commit comments

Comments
 (0)