Skip to content

Commit

Permalink
Issue python#26823: Abbreviate recursive tracebacks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ncoghlan committed Aug 15, 2016
1 parent d61a2e7 commit d003423
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 4 deletions.
15 changes: 15 additions & 0 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------------
Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down Expand Up @@ -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
==========
Expand Down
131 changes: 131 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,137 @@ def prn():
' 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()
Expand Down
23 changes: 23 additions & 0 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,30 @@ def format(self):
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))
Expand All @@ -397,6 +418,8 @@ def format(self):
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


Expand Down
9 changes: 9 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
36 changes: 32 additions & 4 deletions Python/traceback.c
Original file line number Diff line number Diff line change
Expand Up @@ -412,23 +412,51 @@ 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++;
tb1 = tb1->tb_next;
}
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;
}

Expand Down

0 comments on commit d003423

Please sign in to comment.