bpo-45075: distinguish between frame and FrameSummary in traceback mo… (GH-28112)

This commit is contained in:
Irit Katriel 2021-09-03 22:39:23 +01:00 committed by GitHub
parent 6f8bc464e0
commit 0b58e863df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 39 deletions

View File

@ -353,12 +353,12 @@ capture data for later printing in a lightweight fashion.
.. versionchanged:: 3.6 .. versionchanged:: 3.6
Long sequences of repeated frames are now abbreviated. Long sequences of repeated frames are now abbreviated.
.. method:: format_frame(frame) .. method:: format_frame_summary(frame_summary)
Returns a string for printing one of the frames involved in the stack. 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 This method is called for each :class:`FrameSummary` object to be
:class:`StackSummary`. If it returns ``None``, the frame is omitted printed by :meth:`StackSummary.format`. If it returns ``None``, the
from the output. frame is omitted from the output.
.. versionadded:: 3.11 .. versionadded:: 3.11
@ -368,7 +368,7 @@ capture data for later printing in a lightweight fashion.
.. versionadded:: 3.5 .. versionadded:: 3.5
:class:`FrameSummary` objects represent a single frame in a traceback. A :class:`FrameSummary` object represents a single frame in a traceback.
.. class:: FrameSummary(filename, lineno, name, lookup_line=True, locals=None, line=None) .. class:: FrameSummary(filename, lineno, name, lookup_line=True, locals=None, line=None)

View File

@ -1515,8 +1515,8 @@ class TestStack(unittest.TestCase):
def test_custom_format_frame(self): def test_custom_format_frame(self):
class CustomStackSummary(traceback.StackSummary): class CustomStackSummary(traceback.StackSummary):
def format_frame(self, frame): def format_frame_summary(self, frame_summary):
return f'{frame.filename}:{frame.lineno}' return f'{frame_summary.filename}:{frame_summary.lineno}'
def some_inner(): def some_inner():
return CustomStackSummary.extract( return CustomStackSummary.extract(
@ -1540,10 +1540,10 @@ class TestStack(unittest.TestCase):
exc_info = g() exc_info = g()
class Skip_G(traceback.StackSummary): class Skip_G(traceback.StackSummary):
def format_frame(self, frame): def format_frame_summary(self, frame_summary):
if frame.name == 'g': if frame_summary.name == 'g':
return None return None
return super().format_frame(frame) return super().format_frame_summary(frame_summary)
stack = Skip_G.extract( stack = Skip_G.extract(
traceback.walk_tb(exc_info[2])).format() traceback.walk_tb(exc_info[2])).format()

View File

@ -240,7 +240,7 @@ def clear_frames(tb):
class FrameSummary: class FrameSummary:
"""A single frame from a traceback. """Information about a single frame from a traceback.
- :attr:`filename` The filename for the frame. - :attr:`filename` The filename for the frame.
- :attr:`lineno` The line within filename for the frame that was - :attr:`lineno` The line within filename for the frame that was
@ -365,15 +365,15 @@ def _get_code_position(code, instruction_index):
_RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c. _RECURSIVE_CUTOFF = 3 # Also hardcoded in traceback.c.
class StackSummary(list): class StackSummary(list):
"""A stack of frames.""" """A list of FrameSummary objects, representing a stack of frames."""
@classmethod @classmethod
def extract(klass, frame_gen, *, limit=None, lookup_lines=True, def extract(klass, frame_gen, *, limit=None, lookup_lines=True,
capture_locals=False): capture_locals=False):
"""Create a StackSummary from a traceback or stack object. """Create a StackSummary from a traceback or stack object.
:param frame_gen: A generator that yields (frame, lineno) tuples to :param frame_gen: A generator that yields (frame, lineno) tuples
include in the stack. whose summaries are to be included in the stack.
:param limit: None to include all frames or the number of frames to :param limit: None to include all frames or the number of frames to
include. include.
:param lookup_lines: If True, lookup lines for each frame immediately, :param lookup_lines: If True, lookup lines for each frame immediately,
@ -394,7 +394,7 @@ class StackSummary(list):
lookup_lines=True, capture_locals=False): lookup_lines=True, capture_locals=False):
# Same as extract but operates on a frame generator that yields # Same as extract but operates on a frame generator that yields
# (frame, (lineno, end_lineno, colno, end_colno)) in the stack. # (frame, (lineno, end_lineno, colno, end_colno)) in the stack.
# Only lineno is required, the remaining fields can be empty if the # Only lineno is required, the remaining fields can be None if the
# information is not available. # information is not available.
if limit is None: if limit is None:
limit = getattr(sys, 'tracebacklimit', None) limit = getattr(sys, 'tracebacklimit', None)
@ -450,34 +450,38 @@ class StackSummary(list):
result.append(FrameSummary(filename, lineno, name, line=line)) result.append(FrameSummary(filename, lineno, name, line=line))
return result return result
def format_frame(self, frame): def format_frame_summary(self, frame_summary):
"""Format the lines for a single frame. """Format the lines for a single FrameSummary.
Returns a string representing one frame involved in the stack. This Returns a string representing one frame involved in the stack. This
gets called for every frame to be printed in the stack summary. gets called for every frame to be printed in the stack summary.
""" """
row = [] row = []
row.append(' File "{}", line {}, in {}\n'.format( row.append(' File "{}", line {}, in {}\n'.format(
frame.filename, frame.lineno, frame.name)) frame_summary.filename, frame_summary.lineno, frame_summary.name))
if frame.line: if frame_summary.line:
row.append(' {}\n'.format(frame.line.strip())) row.append(' {}\n'.format(frame_summary.line.strip()))
stripped_characters = len(frame._original_line) - len(frame.line.lstrip()) orig_line_len = len(frame_summary._original_line)
frame_line_len = len(frame_summary.line.lstrip())
stripped_characters = orig_line_len - frame_line_len
if ( if (
frame.colno is not None frame_summary.colno is not None
and frame.end_colno is not None and frame_summary.end_colno is not None
): ):
colno = _byte_offset_to_character_offset(frame._original_line, frame.colno) colno = _byte_offset_to_character_offset(
end_colno = _byte_offset_to_character_offset(frame._original_line, frame.end_colno) frame_summary._original_line, frame_summary.colno)
end_colno = _byte_offset_to_character_offset(
frame_summary._original_line, frame_summary.end_colno)
anchors = None anchors = None
if frame.lineno == frame.end_lineno: if frame_summary.lineno == frame_summary.end_lineno:
with suppress(Exception): with suppress(Exception):
anchors = _extract_caret_anchors_from_line_segment( anchors = _extract_caret_anchors_from_line_segment(
frame._original_line[colno - 1:end_colno - 1] frame_summary._original_line[colno - 1:end_colno - 1]
) )
else: else:
end_colno = stripped_characters + len(frame.line.strip()) end_colno = stripped_characters + len(frame_summary.line.strip())
row.append(' ') row.append(' ')
row.append(' ' * (colno - stripped_characters)) row.append(' ' * (colno - stripped_characters))
@ -491,8 +495,8 @@ class StackSummary(list):
row.append('\n') row.append('\n')
if frame.locals: if frame_summary.locals:
for name, value in sorted(frame.locals.items()): for name, value in sorted(frame_summary.locals.items()):
row.append(' {name} = {value}\n'.format(name=name, value=value)) row.append(' {name} = {value}\n'.format(name=name, value=value))
return ''.join(row) return ''.join(row)
@ -514,27 +518,27 @@ class StackSummary(list):
last_line = None last_line = None
last_name = None last_name = None
count = 0 count = 0
for frame in self: for frame_summary in self:
formatted_frame = self.format_frame(frame) formatted_frame = self.format_frame_summary(frame_summary)
if formatted_frame is None: if formatted_frame is None:
continue continue
if (last_file is None or last_file != frame.filename or if (last_file is None or last_file != frame_summary.filename or
last_line is None or last_line != frame.lineno or last_line is None or last_line != frame_summary.lineno or
last_name is None or last_name != frame.name): last_name is None or last_name != frame_summary.name):
if count > _RECURSIVE_CUTOFF: if count > _RECURSIVE_CUTOFF:
count -= _RECURSIVE_CUTOFF count -= _RECURSIVE_CUTOFF
result.append( result.append(
f' [Previous line repeated {count} more ' f' [Previous line repeated {count} more '
f'time{"s" if count > 1 else ""}]\n' f'time{"s" if count > 1 else ""}]\n'
) )
last_file = frame.filename last_file = frame_summary.filename
last_line = frame.lineno last_line = frame_summary.lineno
last_name = frame.name last_name = frame_summary.name
count = 0 count = 0
count += 1 count += 1
if count > _RECURSIVE_CUTOFF: if count > _RECURSIVE_CUTOFF:
continue continue
result.append(self.format_frame(frame)) result.append(formatted_frame)
if count > _RECURSIVE_CUTOFF: if count > _RECURSIVE_CUTOFF:
count -= _RECURSIVE_CUTOFF count -= _RECURSIVE_CUTOFF

View File

@ -0,0 +1,5 @@
Rename :meth:`traceback.StackSummary.format_frame` to
:meth:`traceback.StackSummary.format_frame_summary`. This method was added
for 3.11 so it was not released yet.
Updated code and docs to better distinguish frame and FrameSummary.