Issue #26823: Abbreviate recursive tracebacks
Large sections of repeated lines in tracebacks are now abbreviated as "[Previous line repeated {count} more times]" by both the traceback module and the builtin traceback rendering. Patch by Emanuel Barry.
This commit is contained in:
parent
d61a2e75b5
commit
d00342347e
|
@ -291,6 +291,21 @@ capture data for later printing in a lightweight fashion.
|
|||
of tuples. Each tuple should be a 4-tuple with filename, lineno, name,
|
||||
line as the elements.
|
||||
|
||||
.. method:: format()
|
||||
|
||||
Returns a list of strings ready for printing. Each string in the
|
||||
resulting list corresponds to a single frame from the stack.
|
||||
Each string ends in a newline; the strings may contain internal
|
||||
newlines as well, for those items with source text lines.
|
||||
|
||||
For long sequences of the same frame and line, the first few
|
||||
repetitions are shown, followed by a summary line stating the exact
|
||||
number of further repetitions.
|
||||
|
||||
.. versionchanged:: 3.6
|
||||
|
||||
Long sequences of repeated frames are now abbreviated.
|
||||
|
||||
|
||||
:class:`FrameSummary` Objects
|
||||
-----------------------------
|
||||
|
|
|
@ -438,6 +438,14 @@ not work in future versions of Tcl.
|
|||
(Contributed by Serhiy Storchaka in :issue:`22115`).
|
||||
|
||||
|
||||
traceback
|
||||
---------
|
||||
|
||||
The :meth:`~traceback.StackSummary.format` method now abbreviates long sequences
|
||||
of repeated lines as ``"[Previous line repeated {count} more times]"``.
|
||||
(Contributed by Emanuel Barry in :issue:`26823`.)
|
||||
|
||||
|
||||
typing
|
||||
------
|
||||
|
||||
|
@ -597,6 +605,10 @@ Build and C API Changes
|
|||
defined by empty names.
|
||||
(Contributed by Serhiy Storchaka in :issue:`26282`).
|
||||
|
||||
* ``PyTraceback_Print`` method now abbreviates long sequences of repeated lines
|
||||
as ``"[Previous line repeated {count} more times]"``.
|
||||
(Contributed by Emanuel Barry in :issue:`26823`.)
|
||||
|
||||
|
||||
Deprecated
|
||||
==========
|
||||
|
|
|
@ -303,6 +303,137 @@ class TracebackFormatTests(unittest.TestCase):
|
|||
' traceback.print_stack()',
|
||||
])
|
||||
|
||||
# issue 26823 - Shrink recursive tracebacks
|
||||
def _check_recursive_traceback_display(self, render_exc):
|
||||
# Always show full diffs when this test fails
|
||||
# Note that rearranging things may require adjusting
|
||||
# the relative line numbers in the expected tracebacks
|
||||
self.maxDiff = None
|
||||
|
||||
# Check hitting the recursion limit
|
||||
def f():
|
||||
f()
|
||||
|
||||
with captured_output("stderr") as stderr_f:
|
||||
try:
|
||||
f()
|
||||
except RecursionError as exc:
|
||||
render_exc()
|
||||
else:
|
||||
self.fail("no recursion occurred")
|
||||
|
||||
lineno_f = f.__code__.co_firstlineno
|
||||
result_f = (
|
||||
'Traceback (most recent call last):\n'
|
||||
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
|
||||
' f()\n'
|
||||
f' File "{__file__}", line {lineno_f+1}, in f\n'
|
||||
' f()\n'
|
||||
f' File "{__file__}", line {lineno_f+1}, in f\n'
|
||||
' f()\n'
|
||||
f' File "{__file__}", line {lineno_f+1}, in f\n'
|
||||
' f()\n'
|
||||
# XXX: The following line changes depending on whether the tests
|
||||
# are run through the interactive interpreter or with -m
|
||||
# It also varies depending on the platform (stack size)
|
||||
# Fortunately, we don't care about exactness here, so we use regex
|
||||
r' \[Previous line repeated (\d+) more times\]' '\n'
|
||||
'RecursionError: maximum recursion depth exceeded\n'
|
||||
)
|
||||
|
||||
expected = result_f.splitlines()
|
||||
actual = stderr_f.getvalue().splitlines()
|
||||
|
||||
# Check the output text matches expectations
|
||||
# 2nd last line contains the repetition count
|
||||
self.assertEqual(actual[:-2], expected[:-2])
|
||||
self.assertRegex(actual[-2], expected[-2])
|
||||
self.assertEqual(actual[-1], expected[-1])
|
||||
|
||||
# Check the recursion count is roughly as expected
|
||||
rec_limit = sys.getrecursionlimit()
|
||||
self.assertIn(int(re.search(r"\d+", actual[-2]).group()), range(rec_limit-50, rec_limit))
|
||||
|
||||
# Check a known (limited) number of recursive invocations
|
||||
def g(count=10):
|
||||
if count:
|
||||
return g(count-1)
|
||||
raise ValueError
|
||||
|
||||
with captured_output("stderr") as stderr_g:
|
||||
try:
|
||||
g()
|
||||
except ValueError as exc:
|
||||
render_exc()
|
||||
else:
|
||||
self.fail("no value error was raised")
|
||||
|
||||
lineno_g = g.__code__.co_firstlineno
|
||||
result_g = (
|
||||
f' File "{__file__}", line {lineno_g+2}, in g\n'
|
||||
' return g(count-1)\n'
|
||||
f' File "{__file__}", line {lineno_g+2}, in g\n'
|
||||
' return g(count-1)\n'
|
||||
f' File "{__file__}", line {lineno_g+2}, in g\n'
|
||||
' return g(count-1)\n'
|
||||
' [Previous line repeated 6 more times]\n'
|
||||
f' File "{__file__}", line {lineno_g+3}, in g\n'
|
||||
' raise ValueError\n'
|
||||
'ValueError\n'
|
||||
)
|
||||
tb_line = (
|
||||
'Traceback (most recent call last):\n'
|
||||
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
|
||||
' g()\n'
|
||||
)
|
||||
expected = (tb_line + result_g).splitlines()
|
||||
actual = stderr_g.getvalue().splitlines()
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
# Check 2 different repetitive sections
|
||||
def h(count=10):
|
||||
if count:
|
||||
return h(count-1)
|
||||
g()
|
||||
|
||||
with captured_output("stderr") as stderr_h:
|
||||
try:
|
||||
h()
|
||||
except ValueError as exc:
|
||||
render_exc()
|
||||
else:
|
||||
self.fail("no value error was raised")
|
||||
|
||||
lineno_h = h.__code__.co_firstlineno
|
||||
result_h = (
|
||||
'Traceback (most recent call last):\n'
|
||||
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
|
||||
' h()\n'
|
||||
f' File "{__file__}", line {lineno_h+2}, in h\n'
|
||||
' return h(count-1)\n'
|
||||
f' File "{__file__}", line {lineno_h+2}, in h\n'
|
||||
' return h(count-1)\n'
|
||||
f' File "{__file__}", line {lineno_h+2}, in h\n'
|
||||
' return h(count-1)\n'
|
||||
' [Previous line repeated 6 more times]\n'
|
||||
f' File "{__file__}", line {lineno_h+3}, in h\n'
|
||||
' g()\n'
|
||||
)
|
||||
expected = (result_h + result_g).splitlines()
|
||||
actual = stderr_h.getvalue().splitlines()
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_recursive_traceback_python(self):
|
||||
self._check_recursive_traceback_display(traceback.print_exc)
|
||||
|
||||
@cpython_only
|
||||
def test_recursive_traceback_cpython_internal(self):
|
||||
from _testcapi import exception_print
|
||||
def render_exc():
|
||||
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||
exception_print(exc_value)
|
||||
self._check_recursive_traceback_display(render_exc)
|
||||
|
||||
def test_format_stack(self):
|
||||
def fmt():
|
||||
return traceback.format_stack()
|
||||
|
|
|
@ -385,9 +385,30 @@ class StackSummary(list):
|
|||
resulting list corresponds to a single frame from the stack.
|
||||
Each string ends in a newline; the strings may contain internal
|
||||
newlines as well, for those items with source text lines.
|
||||
|
||||
For long sequences of the same frame and line, the first few
|
||||
repetitions are shown, followed by a summary line stating the exact
|
||||
number of further repetitions.
|
||||
"""
|
||||
result = []
|
||||
last_file = None
|
||||
last_line = None
|
||||
last_name = None
|
||||
count = 0
|
||||
for frame in self:
|
||||
if (last_file is not None and last_file == frame.filename and
|
||||
last_line is not None and last_line == frame.lineno and
|
||||
last_name is not None and last_name == frame.name):
|
||||
count += 1
|
||||
else:
|
||||
if count > 3:
|
||||
result.append(f' [Previous line repeated {count-3} more times]\n')
|
||||
last_file = frame.filename
|
||||
last_line = frame.lineno
|
||||
last_name = frame.name
|
||||
count = 0
|
||||
if count >= 3:
|
||||
continue
|
||||
row = []
|
||||
row.append(' File "{}", line {}, in {}\n'.format(
|
||||
frame.filename, frame.lineno, frame.name))
|
||||
|
@ -397,6 +418,8 @@ class StackSummary(list):
|
|||
for name, value in sorted(frame.locals.items()):
|
||||
row.append(' {name} = {value}\n'.format(name=name, value=value))
|
||||
result.append(''.join(row))
|
||||
if count > 3:
|
||||
result.append(f' [Previous line repeated {count-3} more times]\n')
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ What's New in Python 3.6.0 alpha 4
|
|||
Core and Builtins
|
||||
-----------------
|
||||
|
||||
- Issue #26823: Large sections of repeated lines in tracebacks are now
|
||||
abbreviated as "[Previous line repeated {count} more times]" by the builtin
|
||||
traceback rendering. Patch by Emanuel Barry.
|
||||
|
||||
- Issue #27574: Decreased an overhead of parsing keyword arguments in functions
|
||||
implemented with using Argument Clinic.
|
||||
|
||||
|
@ -46,6 +50,11 @@ Core and Builtins
|
|||
Library
|
||||
-------
|
||||
|
||||
- Issue #26823: traceback.StackSummary.format now abbreviates large sections of
|
||||
repeated lines as "[Previous line repeated {count} more times]" (this change
|
||||
then further affects other traceback display operations in the module). Patch
|
||||
by Emanuel Barry.
|
||||
|
||||
- Issue #27664: Add to concurrent.futures.thread.ThreadPoolExecutor()
|
||||
the ability to specify a thread name prefix.
|
||||
|
||||
|
|
|
@ -412,6 +412,11 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
|
|||
{
|
||||
int err = 0;
|
||||
long depth = 0;
|
||||
PyObject *last_file = NULL;
|
||||
int last_line = -1;
|
||||
PyObject *last_name = NULL;
|
||||
long cnt = 0;
|
||||
PyObject *line;
|
||||
PyTracebackObject *tb1 = tb;
|
||||
while (tb1 != NULL) {
|
||||
depth++;
|
||||
|
@ -419,16 +424,39 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
|
|||
}
|
||||
while (tb != NULL && err == 0) {
|
||||
if (depth <= limit) {
|
||||
err = tb_displayline(f,
|
||||
tb->tb_frame->f_code->co_filename,
|
||||
tb->tb_lineno,
|
||||
tb->tb_frame->f_code->co_name);
|
||||
if (last_file != NULL &&
|
||||
tb->tb_frame->f_code->co_filename == last_file &&
|
||||
last_line != -1 && tb->tb_lineno == last_line &&
|
||||
last_name != NULL &&
|
||||
tb->tb_frame->f_code->co_name == last_name) {
|
||||
cnt++;
|
||||
} else {
|
||||
if (cnt > 3) {
|
||||
line = PyUnicode_FromFormat(
|
||||
" [Previous line repeated %d more times]\n", cnt-3);
|
||||
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
|
||||
}
|
||||
last_file = tb->tb_frame->f_code->co_filename;
|
||||
last_line = tb->tb_lineno;
|
||||
last_name = tb->tb_frame->f_code->co_name;
|
||||
cnt = 0;
|
||||
}
|
||||
if (cnt < 3)
|
||||
err = tb_displayline(f,
|
||||
tb->tb_frame->f_code->co_filename,
|
||||
tb->tb_lineno,
|
||||
tb->tb_frame->f_code->co_name);
|
||||
}
|
||||
depth--;
|
||||
tb = tb->tb_next;
|
||||
if (err == 0)
|
||||
err = PyErr_CheckSignals();
|
||||
}
|
||||
if (cnt > 3) {
|
||||
line = PyUnicode_FromFormat(
|
||||
" [Previous line repeated %d more times]\n", cnt-3);
|
||||
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue