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:
Nick Coghlan 2016-08-15 13:11:34 +10:00
parent d61a2e75b5
commit d00342347e
6 changed files with 222 additions and 4 deletions

View File

@ -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
-----------------------------

View File

@ -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
==========

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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,6 +424,24 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
}
while (tb != NULL && err == 0) {
if (depth <= limit) {
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,
@ -429,6 +452,11 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
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;
}