]> granicus.if.org Git - python/commitdiff
Issue #26823: Abbreviate recursive tracebacks
authorNick Coghlan <ncoghlan@gmail.com>
Mon, 15 Aug 2016 03:11:34 +0000 (13:11 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Mon, 15 Aug 2016 03:11:34 +0000 (13:11 +1000)
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.

Doc/library/traceback.rst
Doc/whatsnew/3.6.rst
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS
Python/traceback.c

index 3c1d9bb51dc60287e1a74b189cbc1d86e6720a3a..533629458f1b3f0494b9f2b5c11122040d1eec21 100644 (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
 -----------------------------
index 43a58a4fda7dcd2775c5c8ecdbaa239287bd9914..9050abccc7e7181be690a1719ae353e90f833be4 100644 (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
 ==========
index 787409c5feb40afaa07641fc9e59229064dc3c42..665abb462b2dbec664a5c7b43c309428d6f1f0b7 100644 (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()
index 3b46c0b050c703386cf6354e1c859d132684aa7b..a1cb5fb1ef15822a3248798bc0303a0e89d27de6 100644 (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
 
 
index 5d4131a3c9c5e37848d4226a51aa962bd1d975a1..9e4b2d01db1a8508f16cb422b5881a3fe79b4c81 100644 (file)
--- 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.
 
index 59552cae85e7929075327e547c749c120b24b1b2..15cde444f40d8740e6c66a6f20c33828102dada7 100644 (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,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;
 }