Skip to content

Commit 34bc594

Browse files
authored
Merge pull request #5133 from kondratyev-nv/fix-handle-repr-error-with-showlocals
Fix handle repr error with showlocals and verbose output
2 parents e87d3d7 + bc00d0f commit 34bc594

File tree

6 files changed

+92
-30
lines changed

6 files changed

+92
-30
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ Nicholas Devenish
180180
Nicholas Murphy
181181
Niclas Olofsson
182182
Nicolas Delaby
183+
Nikolay Kondratyev
183184
Oleg Pidsadnyi
184185
Oleg Sushchenko
185186
Oliver Bestwalter

changelog/5089.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash caused by error in ``__repr__`` function with both ``showlocals`` and verbose output enabled.

src/_pytest/_code/code.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import print_function
44

55
import inspect
6-
import pprint
76
import re
87
import sys
98
import traceback
@@ -18,6 +17,7 @@
1817
from six import text_type
1918

2019
import _pytest
20+
from _pytest._io.saferepr import safeformat
2121
from _pytest._io.saferepr import saferepr
2222
from _pytest.compat import _PY2
2323
from _pytest.compat import _PY3
@@ -614,14 +614,11 @@ def _getentrysource(self, entry):
614614
source = source.deindent()
615615
return source
616616

617-
def _saferepr(self, obj):
618-
return saferepr(obj)
619-
620617
def repr_args(self, entry):
621618
if self.funcargs:
622619
args = []
623620
for argname, argvalue in entry.frame.getargs(var=True):
624-
args.append((argname, self._saferepr(argvalue)))
621+
args.append((argname, saferepr(argvalue)))
625622
return ReprFuncArgs(args)
626623

627624
def get_source(self, source, line_index=-1, excinfo=None, short=False):
@@ -674,9 +671,9 @@ def repr_locals(self, locals):
674671
# _repr() function, which is only reprlib.Repr in
675672
# disguise, so is very configurable.
676673
if self.truncate_locals:
677-
str_repr = self._saferepr(value)
674+
str_repr = saferepr(value)
678675
else:
679-
str_repr = pprint.pformat(value)
676+
str_repr = safeformat(value)
680677
# if len(str_repr) < 70 or not isinstance(value,
681678
# (list, tuple, dict)):
682679
lines.append("%-10s = %s" % (name, str_repr))

src/_pytest/_io/saferepr.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1-
import sys
1+
import pprint
22

33
from six.moves import reprlib
44

55

6+
def _call_and_format_exception(call, x, *args):
7+
try:
8+
# Try the vanilla repr and make sure that the result is a string
9+
return call(x, *args)
10+
except Exception as exc:
11+
exc_name = type(exc).__name__
12+
try:
13+
exc_info = str(exc)
14+
except Exception:
15+
exc_info = "unknown"
16+
return '<[%s("%s") raised in repr()] %s object at 0x%x>' % (
17+
exc_name,
18+
exc_info,
19+
x.__class__.__name__,
20+
id(x),
21+
)
22+
23+
624
class SafeRepr(reprlib.Repr):
725
"""subclass of repr.Repr that limits the resulting size of repr()
826
and includes information on exceptions raised during the call.
@@ -33,28 +51,20 @@ def repr_instance(self, x, level):
3351
return self._callhelper(repr, x)
3452

3553
def _callhelper(self, call, x, *args):
36-
try:
37-
# Try the vanilla repr and make sure that the result is a string
38-
s = call(x, *args)
39-
except Exception:
40-
cls, e, tb = sys.exc_info()
41-
exc_name = getattr(cls, "__name__", "unknown")
42-
try:
43-
exc_info = str(e)
44-
except Exception:
45-
exc_info = "unknown"
46-
return '<[%s("%s") raised in repr()] %s object at 0x%x>' % (
47-
exc_name,
48-
exc_info,
49-
x.__class__.__name__,
50-
id(x),
51-
)
52-
else:
53-
if len(s) > self.maxsize:
54-
i = max(0, (self.maxsize - 3) // 2)
55-
j = max(0, self.maxsize - 3 - i)
56-
s = s[:i] + "..." + s[len(s) - j :]
57-
return s
54+
s = _call_and_format_exception(call, x, *args)
55+
if len(s) > self.maxsize:
56+
i = max(0, (self.maxsize - 3) // 2)
57+
j = max(0, self.maxsize - 3 - i)
58+
s = s[:i] + "..." + s[len(s) - j :]
59+
return s
60+
61+
62+
def safeformat(obj):
63+
"""return a pretty printed string for the given object.
64+
Failing __repr__ functions of user instances will be represented
65+
with a short exception info.
66+
"""
67+
return _call_and_format_exception(pprint.pformat, obj)
5868

5969

6070
def saferepr(obj, maxsize=240):

testing/code/test_excinfo.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,35 @@ def test_repr_local(self):
598598
assert reprlocals.lines[2] == "y = 5"
599599
assert reprlocals.lines[3] == "z = 7"
600600

601+
def test_repr_local_with_error(self):
602+
class ObjWithErrorInRepr:
603+
def __repr__(self):
604+
raise NotImplementedError
605+
606+
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
607+
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
608+
reprlocals = p.repr_locals(loc)
609+
assert reprlocals.lines
610+
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
611+
assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1]
612+
613+
def test_repr_local_with_exception_in_class_property(self):
614+
class ExceptionWithBrokenClass(Exception):
615+
@property
616+
def __class__(self):
617+
raise TypeError("boom!")
618+
619+
class ObjWithErrorInRepr:
620+
def __repr__(self):
621+
raise ExceptionWithBrokenClass()
622+
623+
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
624+
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
625+
reprlocals = p.repr_locals(loc)
626+
assert reprlocals.lines
627+
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
628+
assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1]
629+
601630
def test_repr_local_truncated(self):
602631
loc = {"l": [i for i in range(10)]}
603632
p = FormattedExcinfo(showlocals=True)

testing/test_session.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,30 @@ def test_implicit_bad_repr1(self):
134134
!= -1
135135
)
136136

137+
def test_broken_repr_with_showlocals_verbose(self, testdir):
138+
p = testdir.makepyfile(
139+
"""
140+
class ObjWithErrorInRepr:
141+
def __repr__(self):
142+
raise NotImplementedError
143+
144+
def test_repr_error():
145+
x = ObjWithErrorInRepr()
146+
assert x == "value"
147+
"""
148+
)
149+
reprec = testdir.inline_run("--showlocals", "-vv", p)
150+
passed, skipped, failed = reprec.listoutcomes()
151+
assert (len(passed), len(skipped), len(failed)) == (0, 0, 1)
152+
entries = failed[0].longrepr.reprtraceback.reprentries
153+
assert len(entries) == 1
154+
repr_locals = entries[0].reprlocals
155+
assert repr_locals.lines
156+
assert len(repr_locals.lines) == 1
157+
assert repr_locals.lines[0].startswith(
158+
'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr'
159+
)
160+
137161
def test_skip_file_by_conftest(self, testdir):
138162
testdir.makepyfile(
139163
conftest="""

0 commit comments

Comments
 (0)