mirror of https://github.com/python/cpython
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,
|
of tuples. Each tuple should be a 4-tuple with filename, lineno, name,
|
||||||
line as the elements.
|
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
|
:class:`FrameSummary` Objects
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
|
@ -438,6 +438,14 @@ not work in future versions of Tcl.
|
||||||
(Contributed by Serhiy Storchaka in :issue:`22115`).
|
(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
|
typing
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -597,6 +605,10 @@ Build and C API Changes
|
||||||
defined by empty names.
|
defined by empty names.
|
||||||
(Contributed by Serhiy Storchaka in :issue:`26282`).
|
(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
|
Deprecated
|
||||||
==========
|
==========
|
||||||
|
|
|
@ -303,6 +303,137 @@ class TracebackFormatTests(unittest.TestCase):
|
||||||
' traceback.print_stack()',
|
' 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 test_format_stack(self):
|
||||||
def fmt():
|
def fmt():
|
||||||
return traceback.format_stack()
|
return traceback.format_stack()
|
||||||
|
|
|
@ -385,9 +385,30 @@ class StackSummary(list):
|
||||||
resulting list corresponds to a single frame from the stack.
|
resulting list corresponds to a single frame from the stack.
|
||||||
Each string ends in a newline; the strings may contain internal
|
Each string ends in a newline; the strings may contain internal
|
||||||
newlines as well, for those items with source text lines.
|
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 = []
|
result = []
|
||||||
|
last_file = None
|
||||||
|
last_line = None
|
||||||
|
last_name = None
|
||||||
|
count = 0
|
||||||
for frame in self:
|
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 = []
|
||||||
row.append(' File "{}", line {}, in {}\n'.format(
|
row.append(' File "{}", line {}, in {}\n'.format(
|
||||||
frame.filename, frame.lineno, frame.name))
|
frame.filename, frame.lineno, frame.name))
|
||||||
|
@ -397,6 +418,8 @@ class StackSummary(list):
|
||||||
for name, value in sorted(frame.locals.items()):
|
for name, value in sorted(frame.locals.items()):
|
||||||
row.append(' {name} = {value}\n'.format(name=name, value=value))
|
row.append(' {name} = {value}\n'.format(name=name, value=value))
|
||||||
result.append(''.join(row))
|
result.append(''.join(row))
|
||||||
|
if count > 3:
|
||||||
|
result.append(f' [Previous line repeated {count-3} more times]\n')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ What's New in Python 3.6.0 alpha 4
|
||||||
Core and Builtins
|
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
|
- Issue #27574: Decreased an overhead of parsing keyword arguments in functions
|
||||||
implemented with using Argument Clinic.
|
implemented with using Argument Clinic.
|
||||||
|
|
||||||
|
@ -46,6 +50,11 @@ Core and Builtins
|
||||||
Library
|
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()
|
- Issue #27664: Add to concurrent.futures.thread.ThreadPoolExecutor()
|
||||||
the ability to specify a thread name prefix.
|
the ability to specify a thread name prefix.
|
||||||
|
|
||||||
|
|
|
@ -412,6 +412,11 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
|
||||||
{
|
{
|
||||||
int err = 0;
|
int err = 0;
|
||||||
long depth = 0;
|
long depth = 0;
|
||||||
|
PyObject *last_file = NULL;
|
||||||
|
int last_line = -1;
|
||||||
|
PyObject *last_name = NULL;
|
||||||
|
long cnt = 0;
|
||||||
|
PyObject *line;
|
||||||
PyTracebackObject *tb1 = tb;
|
PyTracebackObject *tb1 = tb;
|
||||||
while (tb1 != NULL) {
|
while (tb1 != NULL) {
|
||||||
depth++;
|
depth++;
|
||||||
|
@ -419,6 +424,24 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
|
||||||
}
|
}
|
||||||
while (tb != NULL && err == 0) {
|
while (tb != NULL && err == 0) {
|
||||||
if (depth <= limit) {
|
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,
|
err = tb_displayline(f,
|
||||||
tb->tb_frame->f_code->co_filename,
|
tb->tb_frame->f_code->co_filename,
|
||||||
tb->tb_lineno,
|
tb->tb_lineno,
|
||||||
|
@ -429,6 +452,11 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
|
||||||
if (err == 0)
|
if (err == 0)
|
||||||
err = PyErr_CheckSignals();
|
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;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue