diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 3c1d9bb51dc..533629458f1 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 43a58a4fda7..9050abccc7e 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 787409c5feb..665abb462b2 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 3b46c0b050c..a1cb5fb1ef1 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 5d4131a3c9c..9e4b2d01db1 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 59552cae85e..15cde444f40 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; }