summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/traceback.rst8
-rw-r--r--Lib/test/test_traceback.py15
-rw-r--r--Lib/traceback.py78
-rw-r--r--Misc/NEWS.d/next/Library/2021-07-08-12-22-54.bpo-44569.KZ02v9.rst3
4 files changed, 70 insertions, 34 deletions
diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst
index be1f43e..83d5c8c 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 402f773..4742eb1 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 40d736a..ae5775d 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()))
-
- 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))
+ result.append(self.format_frame(frame))
+
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 0000000..5f693b2
--- /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.