Skip to content

Commit f136c1e

Browse files
committed
bpo-44569: Decouple frame formatting in traceback.py
1 parent 919ad53 commit f136c1e

File tree

4 files changed

+70
-34
lines changed

4 files changed

+70
-34
lines changed

Doc/library/traceback.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,14 @@ capture data for later printing in a lightweight fashion.
353353
.. versionchanged:: 3.6
354354
Long sequences of repeated frames are now abbreviated.
355355

356+
.. method:: format_frame(frame)
357+
358+
Returns a string for printing one of the frames involved in the stack.
359+
This method gets called for each frame object to be printed in the
360+
:class:`StackSummary`.
361+
362+
.. versionadded:: 3.11
363+
356364

357365
:class:`FrameSummary` Objects
358366
-----------------------------

Lib/test/test_traceback.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,21 @@ def some_inner(k, v):
14291429
' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3)
14301430
], s.format())
14311431

1432+
def test_custom_format_frame(self):
1433+
class CustomStackSummary(traceback.StackSummary):
1434+
def format_frame(self, frame):
1435+
return f'{frame.filename}:{frame.lineno}'
1436+
1437+
def some_inner():
1438+
return CustomStackSummary.extract(
1439+
traceback.walk_stack(None), limit=1)
1440+
1441+
s = some_inner()
1442+
self.assertEqual(
1443+
s.format(),
1444+
[f'{__file__}:{some_inner.__code__.co_firstlineno + 1}'])
1445+
1446+
14321447
class TestTracebackException(unittest.TestCase):
14331448

14341449
def test_smoke(self):

Lib/traceback.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,48 @@ def from_list(klass, a_list):
449449
result.append(FrameSummary(filename, lineno, name, line=line))
450450
return result
451451

452+
def format_frame(self, frame):
453+
"""Format the lines for a single frame.
454+
455+
Returns a string representing one frame involved in the stack. This
456+
gets called for every frame to be printed in the stack summary.
457+
"""
458+
row = []
459+
row.append(' File "{}", line {}, in {}\n'.format(
460+
frame.filename, frame.lineno, frame.name))
461+
if frame.line:
462+
row.append(' {}\n'.format(frame.line.strip()))
463+
464+
stripped_characters = len(frame._original_line) - len(frame.line.lstrip())
465+
if frame.end_lineno == frame.lineno and frame.end_colno != 0:
466+
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
467+
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)
468+
469+
try:
470+
anchors = _extract_caret_anchors_from_line_segment(
471+
frame._original_line[colno - 1:end_colno - 1]
472+
)
473+
except Exception:
474+
anchors = None
475+
476+
row.append(' ')
477+
row.append(' ' * (colno - stripped_characters))
478+
479+
if anchors:
480+
row.append(anchors.primary_char * (anchors.left_end_offset))
481+
row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset))
482+
row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset))
483+
else:
484+
row.append('^' * (end_colno - colno))
485+
486+
row.append('\n')
487+
488+
if frame.locals:
489+
for name, value in sorted(frame.locals.items()):
490+
row.append(' {name} = {value}\n'.format(name=name, value=value))
491+
492+
return ''.join(row)
493+
452494
def format(self):
453495
"""Format the stack ready for printing.
454496
@@ -483,40 +525,8 @@ def format(self):
483525
count += 1
484526
if count > _RECURSIVE_CUTOFF:
485527
continue
486-
row = []
487-
row.append(' File "{}", line {}, in {}\n'.format(
488-
frame.filename, frame.lineno, frame.name))
489-
if frame.line:
490-
row.append(' {}\n'.format(frame.line.strip()))
491-
492-
stripped_characters = len(frame._original_line) - len(frame.line.lstrip())
493-
if frame.end_lineno == frame.lineno and frame.end_colno != 0:
494-
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno)
495-
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno)
496-
497-
try:
498-
anchors = _extract_caret_anchors_from_line_segment(
499-
frame._original_line[colno - 1:end_colno - 1]
500-
)
501-
except Exception:
502-
anchors = None
503-
504-
row.append(' ')
505-
row.append(' ' * (colno - stripped_characters))
506-
507-
if anchors:
508-
row.append(anchors.primary_char * (anchors.left_end_offset))
509-
row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset))
510-
row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset))
511-
else:
512-
row.append('^' * (end_colno - colno))
513-
514-
row.append('\n')
515-
516-
if frame.locals:
517-
for name, value in sorted(frame.locals.items()):
518-
row.append(' {name} = {value}\n'.format(name=name, value=value))
519-
result.append(''.join(row))
528+
result.append(self.format_frame(frame))
529+
520530
if count > _RECURSIVE_CUTOFF:
521531
count -= _RECURSIVE_CUTOFF
522532
result.append(
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added the :func:`StackSummary.format_frame` function in :mod:`traceback`.
2+
This allows users to customize the way individual lines are formatted in
3+
tracebacks without re-implementing logic to handle recursive tracebacks.

0 commit comments

Comments
 (0)