diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -303,6 +303,132 @@ class TracebackFormatTests(unittest.TestCase): ' traceback.print_stack()', ]) + @cpython_only + def test_recursive_functions(self): + # issue 26823 - Shrink recursive tracebacks + from _testcapi import exception_print + f_outputs = [] + g_outputs = [] + h_outputs = [] + def f(): + f() + for i in range(2): + with captured_output("stderr") as _stderr_f: + try: + f() + except RecursionError as exc: + if i == 0: + traceback.print_exc() # Python version + else: + exception_print(exc) # C version + else: + self.fail("no recursion occurred") + f_outputs.append(_stderr_f) + + py_stderr_f, c_stderr_f = f_outputs + + lineno_f = f.__code__.co_firstlineno + result_f = ( + 'Traceback (most recent call last):\n' + ' File "%s", line %d, in test_recursive_functions\n' % (__file__, lineno_f+5) + + ' f()\n' + ' File "%s", line %d, in f\n' % (__file__, lineno_f+1) + + ' f()\n' + ' File "%s", line %d, in f\n' % (__file__, lineno_f+1) + + ' f()\n' + ' File "%s", line %d, in f\n' % (__file__, lineno_f+1) + + ' f()\n' + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + ' [Previous line repeated {0} more times]\n' + 'RecursionError: maximum recursion depth exceeded\n' + ) + if __name__ == "__main__": + result_f = result_f.format(967) + else: + result_f = result_f.format(970) + self.assertEqual(py_stderr_f.getvalue().splitlines(), result_f.splitlines()) # Python version + self.assertEqual(c_stderr_f.getvalue().splitlines(), result_f.splitlines()) # C version + + def g(count=10): + if count: + return g(count-1) + raise ValueError + + for i in range(2): + with captured_output("stderr") as _stderr_g: + try: + g() + except ValueError as exc: + if i == 0: + traceback.print_exc() # Python version + else: + exception_print(exc), # C version + else: + self.fail("no value error was raised") + g_outputs.append(_stderr_g) + + py_stderr_g, c_stderr_g = g_outputs + + lineno_g = g.__code__.co_firstlineno + result_g = ( + ' File "%s", line %d, in g\n' % (__file__, lineno_g+2) + + ' return g(count-1)\n' + ' File "%s", line %d, in g\n' % (__file__, lineno_g+2) + + ' return g(count-1)\n' + ' File "%s", line %d, in g\n' % (__file__, lineno_g+2) + + ' return g(count-1)\n' + ' [Previous line repeated 6 more times]\n' + ' File "%s", line %d, in g\n' % (__file__, lineno_g+3) + + ' raise ValueError\n' + 'ValueError\n' + ) + tb_line = ( + 'Traceback (most recent call last):\n' + ' File "%s", line %d, in test_recursive_functions\n' % (__file__, lineno_g+8) + + ' g()\n' + ) + self.assertEqual(py_stderr_g.getvalue().splitlines(), (tb_line + result_g).splitlines()) # Python version + self.assertEqual(c_stderr_g.getvalue().splitlines(), (tb_line + result_g).splitlines()) # C version + + def h(count=10): + if count: + return h(count-1) + g() + + for i in range(2): + with captured_output("stderr") as _stderr_h: + try: + h() + except ValueError as exc: + if i == 0: + traceback.print_exc() + else: + exception_print(exc) + else: + self.fail("no value error was raised") + h_outputs.append(_stderr_h) + + py_stderr_h, c_stderr_h = h_outputs + + lineno_h = h.__code__.co_firstlineno + result_h = ( + 'Traceback (most recent call last):\n' + ' File "%s", line %d, in test_recursive_functions\n' % (__file__, lineno_h+8) + + ' h()\n' + ' File "%s", line %d, in h\n' % (__file__, lineno_h+2) + + ' return h(count-1)\n' + ' File "%s", line %d, in h\n' % (__file__, lineno_h+2) + + ' return h(count-1)\n' + ' File "%s", line %d, in h\n' % (__file__, lineno_h+2) + + ' return h(count-1)\n' + ' [Previous line repeated 6 more times]\n' + ' File "%s", line %d, in h\n' % (__file__, lineno_h+3) + + ' g()\n' + ) + result_g + self.assertEqual(py_stderr_h.getvalue().splitlines(), result_h.splitlines()) # Python version + self.assertEqual(c_stderr_h.getvalue().splitlines(), result_h.splitlines()) # C version + def test_format_stack(self): def fmt(): return traceback.format_stack() diff --git a/Lib/traceback.py b/Lib/traceback.py --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -387,7 +387,24 @@ class StackSummary(list): newlines as well, for those items with source text lines. """ 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(' [Previous line repeated {} more times]\n'.format(count-3)) + 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 +414,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(' [Previous line repeated {} more times]\n'.format(count-3)) return result diff --git a/Python/traceback.c b/Python/traceback.c --- 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; }