Skip to content

Commit

Permalink
saferepr: handle BaseExceptions (pytest-dev#6047)
Browse files Browse the repository at this point in the history
  • Loading branch information
blueyed authored Nov 7, 2019
2 parents b268463 + fee7c7b commit ab10165
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 23 deletions.
1 change: 1 addition & 0 deletions changelog/6047.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc.
32 changes: 23 additions & 9 deletions src/_pytest/_io/saferepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@
from typing import Any


def _format_repr_exception(exc: Exception, obj: Any) -> str:
exc_name = type(exc).__name__
def _try_repr_or_str(obj):
try:
exc_info = str(exc)
except Exception:
exc_info = "unknown"
return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format(
exc_name, exc_info, obj.__class__.__name__, id(obj)
return repr(obj)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException:
return '{}("{}")'.format(type(obj).__name__, obj)


def _format_repr_exception(exc: BaseException, obj: Any) -> str:
try:
exc_info = _try_repr_or_str(exc)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc))
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
exc_info, obj.__class__.__name__, id(obj)
)


Expand All @@ -35,14 +45,18 @@ def __init__(self, maxsize: int) -> None:
def repr(self, x: Any) -> str:
try:
s = super().repr(x)
except Exception as exc:
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)

def repr_instance(self, x: Any, level: int) -> str:
try:
s = repr(x)
except Exception as exc:
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
s = _format_repr_exception(exc, x)
return _ellipsize(s, self.maxsize)

Expand Down
4 changes: 2 additions & 2 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def __repr__(self):
reprlocals = p.repr_locals(loc)
assert reprlocals.lines
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1]
assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1]

def test_repr_local_with_exception_in_class_property(self):
class ExceptionWithBrokenClass(Exception):
Expand All @@ -602,7 +602,7 @@ def __repr__(self):
reprlocals = p.repr_locals(loc)
assert reprlocals.lines
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1]
assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1]

def test_repr_local_truncated(self):
loc = {"l": [i for i in range(10)]}
Expand Down
79 changes: 76 additions & 3 deletions testing/io/test_saferepr.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from _pytest._io.saferepr import saferepr


Expand Down Expand Up @@ -40,9 +41,81 @@ class BrokenReprException(Exception):
assert "TypeError" in s
assert "TypeError" in saferepr(BrokenRepr("string"))

s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse")))
assert "NameError" not in s2
assert "unknown" in s2
none = None
try:
none()
except BaseException as exc:
exp_exc = repr(exc)
obj = BrokenRepr(BrokenReprException("omg even worse"))
s2 = saferepr(obj)
assert s2 == (
"<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format(
exp_exc, id(obj)
)
)


def test_baseexception():
"""Test saferepr() with BaseExceptions, which includes pytest outcomes."""

class RaisingOnStrRepr(BaseException):
def __init__(self, exc_types):
self.exc_types = exc_types

def raise_exc(self, *args):
try:
self.exc_type = self.exc_types.pop(0)
except IndexError:
pass
if hasattr(self.exc_type, "__call__"):
raise self.exc_type(*args)
raise self.exc_type

def __str__(self):
self.raise_exc("__str__")

def __repr__(self):
self.raise_exc("__repr__")

class BrokenObj:
def __init__(self, exc):
self.exc = exc

def __repr__(self):
raise self.exc

__str__ = __repr__

baseexc_str = BaseException("__str__")
obj = BrokenObj(RaisingOnStrRepr([BaseException]))
assert saferepr(obj) == (
"<[unpresentable exception ({!r}) "
"raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj))
)
obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])]))
assert saferepr(obj) == (
"<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format(
baseexc_str, id(obj)
)
)

with pytest.raises(KeyboardInterrupt):
saferepr(BrokenObj(KeyboardInterrupt()))

with pytest.raises(SystemExit):
saferepr(BrokenObj(SystemExit()))

with pytest.raises(KeyboardInterrupt):
saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt])))

with pytest.raises(SystemExit):
saferepr(BrokenObj(RaisingOnStrRepr([SystemExit])))

with pytest.raises(KeyboardInterrupt):
print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt]))))

with pytest.raises(SystemExit):
saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit])))


def test_buggy_builtin_repr():
Expand Down
18 changes: 9 additions & 9 deletions testing/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,20 @@ def test_broken_repr(self, testdir):
p = testdir.makepyfile(
"""
import pytest
class reprexc(BaseException):
def __str__(self):
return "Ha Ha fooled you, I'm a broken repr()."
class BrokenRepr1(object):
foo=0
def __repr__(self):
raise Exception("Ha Ha fooled you, I'm a broken repr().")
raise reprexc
class TestBrokenClass(object):
def test_explicit_bad_repr(self):
t = BrokenRepr1()
with pytest.raises(Exception, match="I'm a broken repr"):
with pytest.raises(BaseException, match="broken repr"):
repr(t)
def test_implicit_bad_repr1(self):
Expand All @@ -123,12 +128,7 @@ def test_implicit_bad_repr1(self):
passed, skipped, failed = reprec.listoutcomes()
assert (len(passed), len(skipped), len(failed)) == (1, 0, 1)
out = failed[0].longrepr.reprcrash.message
assert (
out.find(
"""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]"""
)
!= -1
)
assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1

def test_broken_repr_with_showlocals_verbose(self, testdir):
p = testdir.makepyfile(
Expand All @@ -151,7 +151,7 @@ def test_repr_error():
assert repr_locals.lines
assert len(repr_locals.lines) == 1
assert repr_locals.lines[0].startswith(
'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr'
"x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr"
)

def test_skip_file_by_conftest(self, testdir):
Expand Down

0 comments on commit ab10165

Please sign in to comment.