Skip to content

Commit 917b9a8

Browse files
Merge pull request #2476 from nicoddemus/fix-2459-numpy-comparison
Fix internal error when a recursion error occurs and frames contain objects that can't be compared
2 parents d2db662 + 2127a23 commit 917b9a8

File tree

3 files changed

+68
-7
lines changed

3 files changed

+68
-7
lines changed

_pytest/_code/code.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from inspect import CO_VARARGS, CO_VARKEYWORDS
44
import re
55
from weakref import ref
6-
from _pytest.compat import _PY2, _PY3, PY35
6+
from _pytest.compat import _PY2, _PY3, PY35, safe_str
77

88
import py
99
builtin_repr = repr
@@ -602,21 +602,48 @@ def repr_traceback(self, excinfo):
602602
traceback = excinfo.traceback
603603
if self.tbfilter:
604604
traceback = traceback.filter()
605-
recursionindex = None
605+
606606
if is_recursion_error(excinfo):
607-
recursionindex = traceback.recursionindex()
607+
traceback, extraline = self._truncate_recursive_traceback(traceback)
608+
else:
609+
extraline = None
610+
608611
last = traceback[-1]
609612
entries = []
610-
extraline = None
611613
for index, entry in enumerate(traceback):
612614
einfo = (last == entry) and excinfo or None
613615
reprentry = self.repr_traceback_entry(entry, einfo)
614616
entries.append(reprentry)
615-
if index == recursionindex:
616-
extraline = "!!! Recursion detected (same locals & position)"
617-
break
618617
return ReprTraceback(entries, extraline, style=self.style)
619618

619+
def _truncate_recursive_traceback(self, traceback):
620+
"""
621+
Truncate the given recursive traceback trying to find the starting point
622+
of the recursion.
623+
624+
The detection is done by going through each traceback entry and finding the
625+
point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
626+
627+
Handle the situation where the recursion process might raise an exception (for example
628+
comparing numpy arrays using equality raises a TypeError), in which case we do our best to
629+
warn the user of the error and show a limited traceback.
630+
"""
631+
try:
632+
recursionindex = traceback.recursionindex()
633+
except Exception as e:
634+
max_frames = 10
635+
extraline = (
636+
'!!! Recursion error detected, but an error occurred locating the origin of recursion.\n'
637+
' The following exception happened when comparing locals in the stack frame:\n'
638+
' {exc_type}: {exc_msg}\n'
639+
' Displaying first and last {max_frames} stack frames out of {total}.'
640+
).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback))
641+
traceback = traceback[:max_frames] + traceback[-max_frames:]
642+
else:
643+
extraline = "!!! Recursion detected (same locals & position)"
644+
traceback = traceback[:recursionindex + 1]
645+
646+
return traceback, extraline
620647

621648
def repr_excinfo(self, excinfo):
622649
if _PY2:

changelog/2459.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix recursion error detection when frames in the traceback contain objects that can't be compared (like ``numpy`` arrays).

testing/code/test_excinfo.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,3 +1140,36 @@ def test(tmpdir):
11401140
result = testdir.runpytest()
11411141
result.stdout.fnmatch_lines(['* 1 failed in *'])
11421142
assert 'INTERNALERROR' not in result.stdout.str() + result.stderr.str()
1143+
1144+
1145+
def test_exception_repr_extraction_error_on_recursion():
1146+
"""
1147+
Ensure we can properly detect a recursion error even
1148+
if some locals raise error on comparision (#2459).
1149+
"""
1150+
class numpy_like(object):
1151+
1152+
def __eq__(self, other):
1153+
if type(other) is numpy_like:
1154+
raise ValueError('The truth value of an array '
1155+
'with more than one element is ambiguous.')
1156+
1157+
def a(x):
1158+
return b(numpy_like())
1159+
1160+
def b(x):
1161+
return a(numpy_like())
1162+
1163+
try:
1164+
a(numpy_like())
1165+
except:
1166+
from _pytest._code.code import ExceptionInfo
1167+
from _pytest.pytester import LineMatcher
1168+
exc_info = ExceptionInfo()
1169+
1170+
matcher = LineMatcher(str(exc_info.getrepr()).splitlines())
1171+
matcher.fnmatch_lines([
1172+
'!!! Recursion error detected, but an error occurred locating the origin of recursion.',
1173+
'*The following exception happened*',
1174+
'*ValueError: The truth value of an array*',
1175+
])

0 commit comments

Comments
 (0)