Skip to content

Commit 3d2a091

Browse files
committed
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.
1 parent 518f029 commit 3d2a091

6 files changed

Lines changed: 222 additions & 4 deletions

File tree

Doc/library/traceback.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,21 @@ capture data for later printing in a lightweight fashion.
291291
of tuples. Each tuple should be a 4-tuple with filename, lineno, name,
292292
line as the elements.
293293

294+
.. method:: format()
295+
296+
Returns a list of strings ready for printing. Each string in the
297+
resulting list corresponds to a single frame from the stack.
298+
Each string ends in a newline; the strings may contain internal
299+
newlines as well, for those items with source text lines.
300+
301+
For long sequences of the same frame and line, the first few
302+
repetitions are shown, followed by a summary line stating the exact
303+
number of further repetitions.
304+
305+
.. versionchanged:: 3.6
306+
307+
Long sequences of repeated frames are now abbreviated.
308+
294309

295310
:class:`FrameSummary` Objects
296311
-----------------------------

Doc/whatsnew/3.6.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,14 @@ not work in future versions of Tcl.
438438
(Contributed by Serhiy Storchaka in :issue:`22115`).
439439

440440

441+
traceback
442+
---------
443+
444+
The :meth:`~traceback.StackSummary.format` method now abbreviates long sequences
445+
of repeated lines as ``"[Previous line repeated {count} more times]"``.
446+
(Contributed by Emanuel Barry in :issue:`26823`.)
447+
448+
441449
typing
442450
------
443451

@@ -597,6 +605,10 @@ Build and C API Changes
597605
defined by empty names.
598606
(Contributed by Serhiy Storchaka in :issue:`26282`).
599607

608+
* ``PyTraceback_Print`` method now abbreviates long sequences of repeated lines
609+
as ``"[Previous line repeated {count} more times]"``.
610+
(Contributed by Emanuel Barry in :issue:`26823`.)
611+
600612

601613
Deprecated
602614
==========

Lib/test/test_traceback.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,137 @@ def prn():
303303
' traceback.print_stack()',
304304
])
305305

306+
# issue 26823 - Shrink recursive tracebacks
307+
def _check_recursive_traceback_display(self, render_exc):
308+
# Always show full diffs when this test fails
309+
# Note that rearranging things may require adjusting
310+
# the relative line numbers in the expected tracebacks
311+
self.maxDiff = None
312+
313+
# Check hitting the recursion limit
314+
def f():
315+
f()
316+
317+
with captured_output("stderr") as stderr_f:
318+
try:
319+
f()
320+
except RecursionError as exc:
321+
render_exc()
322+
else:
323+
self.fail("no recursion occurred")
324+
325+
lineno_f = f.__code__.co_firstlineno
326+
result_f = (
327+
'Traceback (most recent call last):\n'
328+
f' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n'
329+
' f()\n'
330+
f' File "{__file__}", line {lineno_f+1}, in f\n'
331+
' f()\n'
332+
f' File "{__file__}", line {lineno_f+1}, in f\n'
333+
' f()\n'
334+
f' File "{__file__}", line {lineno_f+1}, in f\n'
335+
' f()\n'
336+
# XXX: The following line changes depending on whether the tests
337+
# are run through the interactive interpreter or with -m
338+
# It also varies depending on the platform (stack size)
339+
# Fortunately, we don't care about exactness here, so we use regex
340+
r' \[Previous line repeated (\d+) more times\]' '\n'
341+
'RecursionError: maximum recursion depth exceeded\n'
342+
)
343+
344+
expected = result_f.splitlines()
345+
actual = stderr_f.getvalue().splitlines()
346+
347+
# Check the output text matches expectations
348+
# 2nd last line contains the repetition count
349+
self.assertEqual(actual[:-2], expected[:-2])
350+
self.assertRegex(actual[-2], expected[-2])
351+
self.assertEqual(actual[-1], expected[-1])
352+
353+
# Check the recursion count is roughly as expected
354+
rec_limit = sys.getrecursionlimit()
355+
self.assertIn(int(re.search(r"\d+", actual[-2]).group()), range(rec_limit-50, rec_limit))
356+
357+
# Check a known (limited) number of recursive invocations
358+
def g(count=10):
359+
if count:
360+
return g(count-1)
361+
raise ValueError
362+
363+
with captured_output("stderr") as stderr_g:
364+
try:
365+
g()
366+
except ValueError as exc:
367+
render_exc()
368+
else:
369+
self.fail("no value error was raised")
370+
371+
lineno_g = g.__code__.co_firstlineno
372+
result_g = (
373+
f' File "{__file__}", line {lineno_g+2}, in g\n'
374+
' return g(count-1)\n'
375+
f' File "{__file__}", line {lineno_g+2}, in g\n'
376+
' return g(count-1)\n'
377+
f' File "{__file__}", line {lineno_g+2}, in g\n'
378+
' return g(count-1)\n'
379+
' [Previous line repeated 6 more times]\n'
380+
f' File "{__file__}", line {lineno_g+3}, in g\n'
381+
' raise ValueError\n'
382+
'ValueError\n'
383+
)
384+
tb_line = (
385+
'Traceback (most recent call last):\n'
386+
f' File "{__file__}", line {lineno_g+7}, in _check_recursive_traceback_display\n'
387+
' g()\n'
388+
)
389+
expected = (tb_line + result_g).splitlines()
390+
actual = stderr_g.getvalue().splitlines()
391+
self.assertEqual(actual, expected)
392+
393+
# Check 2 different repetitive sections
394+
def h(count=10):
395+
if count:
396+
return h(count-1)
397+
g()
398+
399+
with captured_output("stderr") as stderr_h:
400+
try:
401+
h()
402+
except ValueError as exc:
403+
render_exc()
404+
else:
405+
self.fail("no value error was raised")
406+
407+
lineno_h = h.__code__.co_firstlineno
408+
result_h = (
409+
'Traceback (most recent call last):\n'
410+
f' File "{__file__}", line {lineno_h+7}, in _check_recursive_traceback_display\n'
411+
' h()\n'
412+
f' File "{__file__}", line {lineno_h+2}, in h\n'
413+
' return h(count-1)\n'
414+
f' File "{__file__}", line {lineno_h+2}, in h\n'
415+
' return h(count-1)\n'
416+
f' File "{__file__}", line {lineno_h+2}, in h\n'
417+
' return h(count-1)\n'
418+
' [Previous line repeated 6 more times]\n'
419+
f' File "{__file__}", line {lineno_h+3}, in h\n'
420+
' g()\n'
421+
)
422+
expected = (result_h + result_g).splitlines()
423+
actual = stderr_h.getvalue().splitlines()
424+
self.assertEqual(actual, expected)
425+
426+
def test_recursive_traceback_python(self):
427+
self._check_recursive_traceback_display(traceback.print_exc)
428+
429+
@cpython_only
430+
def test_recursive_traceback_cpython_internal(self):
431+
from _testcapi import exception_print
432+
def render_exc():
433+
exc_type, exc_value, exc_tb = sys.exc_info()
434+
exception_print(exc_value)
435+
self._check_recursive_traceback_display(render_exc)
436+
306437
def test_format_stack(self):
307438
def fmt():
308439
return traceback.format_stack()

Lib/traceback.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,30 @@ def format(self):
385385
resulting list corresponds to a single frame from the stack.
386386
Each string ends in a newline; the strings may contain internal
387387
newlines as well, for those items with source text lines.
388+
389+
For long sequences of the same frame and line, the first few
390+
repetitions are shown, followed by a summary line stating the exact
391+
number of further repetitions.
388392
"""
389393
result = []
394+
last_file = None
395+
last_line = None
396+
last_name = None
397+
count = 0
390398
for frame in self:
399+
if (last_file is not None and last_file == frame.filename and
400+
last_line is not None and last_line == frame.lineno and
401+
last_name is not None and last_name == frame.name):
402+
count += 1
403+
else:
404+
if count > 3:
405+
result.append(f' [Previous line repeated {count-3} more times]\n')
406+
last_file = frame.filename
407+
last_line = frame.lineno
408+
last_name = frame.name
409+
count = 0
410+
if count >= 3:
411+
continue
391412
row = []
392413
row.append(' File "{}", line {}, in {}\n'.format(
393414
frame.filename, frame.lineno, frame.name))
@@ -397,6 +418,8 @@ def format(self):
397418
for name, value in sorted(frame.locals.items()):
398419
row.append(' {name} = {value}\n'.format(name=name, value=value))
399420
result.append(''.join(row))
421+
if count > 3:
422+
result.append(f' [Previous line repeated {count-3} more times]\n')
400423
return result
401424

402425

Misc/NEWS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ What's New in Python 3.6.0 alpha 4
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #26823: Large sections of repeated lines in tracebacks are now
14+
abbreviated as "[Previous line repeated {count} more times]" by the builtin
15+
traceback rendering. Patch by Emanuel Barry.
16+
1317
- Issue #27574: Decreased an overhead of parsing keyword arguments in functions
1418
implemented with using Argument Clinic.
1519

@@ -46,6 +50,11 @@ Core and Builtins
4650
Library
4751
-------
4852

53+
- Issue #26823: traceback.StackSummary.format now abbreviates large sections of
54+
repeated lines as "[Previous line repeated {count} more times]" (this change
55+
then further affects other traceback display operations in the module). Patch
56+
by Emanuel Barry.
57+
4958
- Issue #27664: Add to concurrent.futures.thread.ThreadPoolExecutor()
5059
the ability to specify a thread name prefix.
5160

Python/traceback.c

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,23 +412,51 @@ tb_printinternal(PyTracebackObject *tb, PyObject *f, long limit)
412412
{
413413
int err = 0;
414414
long depth = 0;
415+
PyObject *last_file = NULL;
416+
int last_line = -1;
417+
PyObject *last_name = NULL;
418+
long cnt = 0;
419+
PyObject *line;
415420
PyTracebackObject *tb1 = tb;
416421
while (tb1 != NULL) {
417422
depth++;
418423
tb1 = tb1->tb_next;
419424
}
420425
while (tb != NULL && err == 0) {
421426
if (depth <= limit) {
422-
err = tb_displayline(f,
423-
tb->tb_frame->f_code->co_filename,
424-
tb->tb_lineno,
425-
tb->tb_frame->f_code->co_name);
427+
if (last_file != NULL &&
428+
tb->tb_frame->f_code->co_filename == last_file &&
429+
last_line != -1 && tb->tb_lineno == last_line &&
430+
last_name != NULL &&
431+
tb->tb_frame->f_code->co_name == last_name) {
432+
cnt++;
433+
} else {
434+
if (cnt > 3) {
435+
line = PyUnicode_FromFormat(
436+
" [Previous line repeated %d more times]\n", cnt-3);
437+
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
438+
}
439+
last_file = tb->tb_frame->f_code->co_filename;
440+
last_line = tb->tb_lineno;
441+
last_name = tb->tb_frame->f_code->co_name;
442+
cnt = 0;
443+
}
444+
if (cnt < 3)
445+
err = tb_displayline(f,
446+
tb->tb_frame->f_code->co_filename,
447+
tb->tb_lineno,
448+
tb->tb_frame->f_code->co_name);
426449
}
427450
depth--;
428451
tb = tb->tb_next;
429452
if (err == 0)
430453
err = PyErr_CheckSignals();
431454
}
455+
if (cnt > 3) {
456+
line = PyUnicode_FromFormat(
457+
" [Previous line repeated %d more times]\n", cnt-3);
458+
err = PyFile_WriteObject(line, f, Py_PRINT_RAW);
459+
}
432460
return err;
433461
}
434462

0 commit comments

Comments
 (0)