diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index be1f43ea953..83d5c8c6fcb 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -353,6 +353,14 @@ capture data for later printing in a lightweight fashion. .. versionchanged:: 3.6 Long sequences of repeated frames are now abbreviated. + .. method:: format_frame(frame) + + Returns a string for printing one of the frames involved in the stack. + This method gets called for each frame object to be printed in the + :class:`StackSummary`. + + .. versionadded:: 3.11 + :class:`FrameSummary` Objects ----------------------------- diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 402f773814e..4742eb1d230 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1429,6 +1429,21 @@ class TestStack(unittest.TestCase): ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3) ], s.format()) + def test_custom_format_frame(self): + class CustomStackSummary(traceback.StackSummary): + def format_frame(self, frame): + return f'{frame.filename}:{frame.lineno}' + + def some_inner(): + return CustomStackSummary.extract( + traceback.walk_stack(None), limit=1) + + s = some_inner() + self.assertEqual( + s.format(), + [f'{__file__}:{some_inner.__code__.co_firstlineno + 1}']) + + class TestTracebackException(unittest.TestCase): def test_smoke(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 40d736af56d..ae5775d2f3b 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -449,6 +449,48 @@ class StackSummary(list): result.append(FrameSummary(filename, lineno, name, line=line)) return result + def format_frame(self, frame): + """Format the lines for a single frame. + + Returns a string representing one frame involved in the stack. This + gets called for every frame to be printed in the stack summary. + """ + row = [] + row.append(' File "{}", line {}, in {}\n'.format( + frame.filename, frame.lineno, frame.name)) + if frame.line: + row.append(' {}\n'.format(frame.line.strip())) + + stripped_characters = len(frame._original_line) - len(frame.line.lstrip()) + if frame.end_lineno == frame.lineno and frame.end_colno != 0: + colno = _byte_offset_to_character_offset(frame._original_line, frame.colno) + end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno) + + try: + anchors = _extract_caret_anchors_from_line_segment( + frame._original_line[colno - 1:end_colno - 1] + ) + except Exception: + anchors = None + + row.append(' ') + row.append(' ' * (colno - stripped_characters)) + + if anchors: + row.append(anchors.primary_char * (anchors.left_end_offset)) + row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset)) + row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset)) + else: + row.append('^' * (end_colno - colno)) + + row.append('\n') + + if frame.locals: + for name, value in sorted(frame.locals.items()): + row.append(' {name} = {value}\n'.format(name=name, value=value)) + + return ''.join(row) + def format(self): """Format the stack ready for printing. @@ -483,40 +525,8 @@ class StackSummary(list): count += 1 if count > _RECURSIVE_CUTOFF: continue - row = [] - row.append(' File "{}", line {}, in {}\n'.format( - frame.filename, frame.lineno, frame.name)) - if frame.line: - row.append(' {}\n'.format(frame.line.strip())) + result.append(self.format_frame(frame)) - stripped_characters = len(frame._original_line) - len(frame.line.lstrip()) - if frame.end_lineno == frame.lineno and frame.end_colno != 0: - colno = _byte_offset_to_character_offset(frame._original_line, frame.colno) - end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno) - - try: - anchors = _extract_caret_anchors_from_line_segment( - frame._original_line[colno - 1:end_colno - 1] - ) - except Exception: - anchors = None - - row.append(' ') - row.append(' ' * (colno - stripped_characters)) - - if anchors: - row.append(anchors.primary_char * (anchors.left_end_offset)) - row.append(anchors.secondary_char * (anchors.right_start_offset - anchors.left_end_offset)) - row.append(anchors.primary_char * (end_colno - colno - anchors.right_start_offset)) - else: - row.append('^' * (end_colno - colno)) - - row.append('\n') - - if frame.locals: - for name, value in sorted(frame.locals.items()): - row.append(' {name} = {value}\n'.format(name=name, value=value)) - result.append(''.join(row)) if count > _RECURSIVE_CUTOFF: count -= _RECURSIVE_CUTOFF result.append( diff --git a/Misc/NEWS.d/next/Library/2021-07-08-12-22-54.bpo-44569.KZ02v9.rst b/Misc/NEWS.d/next/Library/2021-07-08-12-22-54.bpo-44569.KZ02v9.rst new file mode 100644 index 00000000000..5f693b290df --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-08-12-22-54.bpo-44569.KZ02v9.rst @@ -0,0 +1,3 @@ +Added the :func:`StackSummary.format_frame` function in :mod:`traceback`. +This allows users to customize the way individual lines are formatted in +tracebacks without re-implementing logic to handle recursive tracebacks.