Skip to content

Commit

Permalink
python: collect: ignore exceptions with isinstance
Browse files Browse the repository at this point in the history
  • Loading branch information
blueyed committed Nov 1, 2018
1 parent 56e6bb0 commit e30f709
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/4266.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle (ignore) exceptions raised during collection, e.g. with Django's LazySettings proxy class.
8 changes: 8 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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
Expand Down Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys

import six

import pytest
from _pytest.outcomes import Failed

Expand Down Expand Up @@ -170,3 +172,25 @@ class ClassLooksIterableException(Exception):
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]
27 changes: 27 additions & 0 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,3 +977,30 @@ def fix():
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*"])
12 changes: 12 additions & 0 deletions testing/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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


Expand Down Expand Up @@ -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

0 comments on commit e30f709

Please sign in to comment.