summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Coghlan <ncoghlan@gmail.com>2016-08-15 03:11:34 (GMT)
committerNick Coghlan <ncoghlan@gmail.com>2016-08-15 03:11:34 (GMT)
commitd00342347e467981b52368235b99a22dc264dab1 (patch)
treee7ea356a2c4e93f592fd7f3e52e12dfeccfaf13e
parentd61a2e75b5218f0f89b4f623713004edb9512180 (diff)
downloadcpython-d00342347e467981b52368235b99a22dc264dab1.zip
cpython-d00342347e467981b52368235b99a22dc264dab1.tar.gz
cpython-d00342347e467981b52368235b99a22dc264dab1.tar.bz2
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.
-rw-r--r--Doc/library/traceback.rst15
-rw-r--r--Doc/whatsnew/3.6.rst12
-rw-r--r--Lib/test/test_traceback.py131
-rw-r--r--Lib/traceback.py23
-rw-r--r--Misc/NEWS9
-rw-r--r--Python/traceback.c36
6 files changed, 222 insertions, 4 deletions
diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst
index 3c1d9bb..5336294 100644
--- a/Doc/library/traceback.rst
+++ b/Doc/library/traceback.rst
@@ -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
-----------------------------
diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst
index 43a58a4..9050abc 100644
--- a/Doc/whatsnew/3.6.rst
+++ b/Doc/whatsnew/3.6.rst
@@ -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
==========
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 787409c..665abb4 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -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()
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 3b46c0b..a1cb5fb 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -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
diff --git a/Misc/NEWS b/Misc/NEWS
index 5d4131a..9e4b2d0 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -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.
diff --git a/Python/traceback.c b/Python/traceback.c
index 59552ca..15cde44 100644
--- a/Python/traceback.c
+++ b/Python/traceback.c
@@ -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;
}