Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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