From 67bab37b2dca1ce87e0e053348773051793599a1 Mon Sep 17 00:00:00 2001 From: harjoth Date: Thu, 9 Jul 2026 17:31:14 -0700 Subject: [PATCH 1/2] gh-140456: Clarify traceback source line documentation --- Doc/library/traceback.rst | 19 +++++++++++++------ Lib/test/test_traceback.py | 33 +++++++++++++++++++++++++++++++++ Lib/traceback.py | 23 ++++++++++++++++------- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index aa48cea357cfd34..770ffb9814591c2 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -171,9 +171,11 @@ Module-Level Functions Given a list of tuples or :class:`FrameSummary` objects as returned by :func:`extract_tb` or :func:`extract_stack`, return a list of strings ready for printing. Each string in the resulting list corresponds to the item with - the same index in the argument list. Each string ends in a newline; the - strings may contain internal newlines as well, for those items whose source - text line is not ``None``. + the same index in the argument list. Each string ends in a newline and may + contain internal newlines. When an item's source text spans multiple + physical lines, only the first physical line is displayed if column position + information is unavailable. Old-style tuples never have column position + information. .. function:: format_exception_only(exc, /[, value], *, show_group=False) @@ -471,14 +473,19 @@ the module-level functions described above. :class:`FrameSummary` objects or old-style list of tuples. Each tuple should be a 4-tuple with *filename*, *lineno*, *name*, *line* as the elements. + Old-style tuples do not have column position information, so only the + first physical line of *line* is displayed when they are formatted. The + same applies to :class:`FrameSummary` objects without column position + information. .. method:: format() Returns a list of strings ready for printing. Each string in the resulting list corresponds to a single :ref:`frame ` from the stack. - Each string ends in a newline; the strings may contain internal - newlines as well, for those items with source text lines. + Each string ends in a newline and may contain internal newlines. When a + frame's source text spans multiple physical lines, only the first physical + line is displayed if column position information is unavailable. For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact @@ -540,7 +547,7 @@ in a :ref:`traceback `. .. attribute:: FrameSummary.line - A string representing the source code for this frame, with leading and + The first physical line of source code for this frame, with leading and trailing whitespace stripped. If the source is not available, it is ``None``. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index e38d0942e463e9c..17c69768625718a 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3354,6 +3354,10 @@ def test_explicit_line(self): f = traceback.FrameSummary("f", 1, "dummy", line="line") self.assertEqual("line", f.line) + def test_explicit_multiline_line(self): + f = traceback.FrameSummary("f", 1, "dummy", line="line 1\nline 2") + self.assertEqual("line 1", f.line) + def test_len(self): f = traceback.FrameSummary("f", 1, "dummy", line="line") self.assertEqual(len(f), 4) @@ -3415,6 +3419,35 @@ def test_from_list(self): [' File "foo.py", line 1, in fred\n line\n'], s.format()) + def test_from_list_multiline_without_columns(self): + frame = traceback.FrameSummary( + 'foo.py', 1, 'fred', line='line 1\nline 2') + s = traceback.StackSummary.from_list([frame]) + self.assertEqual( + [' File "foo.py", line 1, in fred\n line 1\n'], + s.format()) + + def test_from_list_multiline_with_columns(self): + frame = traceback.FrameSummary( + 'foo.py', 1, 'fred', line='line 1\nline 2', + end_lineno=2, colno=0, end_colno=6) + s = traceback.StackSummary.from_list([frame]) + self.assertEqual( + [' File "foo.py", line 1, in fred\n' + ' line 1\n' + ' line 2\n'], + s.format()) + + def test_format_list_multiline_old_style_tuple(self): + frames = [ + ('test.py', 10, 'func', + 'x = foo + bar\n\t\thello\n\tworld'), + ] + self.assertEqual( + [' File "test.py", line 10, in func\n' + ' x = foo + bar\n'], + traceback.format_list(frames)) + def test_from_list_edited_stack(self): s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) s[0] = ('foo.py', 2, 'fred', 'line') diff --git a/Lib/traceback.py b/Lib/traceback.py index dcdab1f12e9a168..5d9cf0936184d12 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -83,9 +83,11 @@ def format_list(extracted_list): for printing. Each string in the resulting list corresponds to the item with the - same index in the argument list. Each string ends in a newline; - the strings may contain internal newlines as well, for those items - whose source text line is not None. + same index in the argument list. Each string ends in a newline and may + contain internal newlines. When an item's source text spans multiple + physical lines, only the first physical line is displayed if column + position information is unavailable. Old-style tuples never have column + position information. """ return StackSummary.from_list(extracted_list).format() @@ -324,8 +326,8 @@ class FrameSummary: active when the frame was captured. - :attr:`name` The name of the function or method that was executing when the frame was captured. - - :attr:`line` The text from the linecache module for the line - of code that was running when the frame was captured. + - :attr:`line` The first physical line of code that was running when + the frame was captured. - :attr:`locals` Either None if locals were not supplied, or a dict mapping the name to the repr() of the variable. - :attr:`end_lineno` The last line number of the source code for this frame. @@ -558,6 +560,11 @@ def from_list(klass, a_list): """ Create a StackSummary object from a supplied list of FrameSummary objects or old-style list of tuples. + + Old-style tuples do not have column position information, so only the + first physical line of the line element is displayed when they are + formatted. The same applies to FrameSummary objects without column + position information. """ # While doing a fast-path check for isinstance(a_list, StackSummary) is # appealing, idlelib.run.cleanup_traceback and other similar code may @@ -794,8 +801,10 @@ def format(self, **kwargs): Returns a list of strings ready for printing. Each string in the resulting list corresponds to a single frame from the stack. - Each string ends in a newline; the strings may contain internal - newlines as well, for those items with source text lines. + Each string ends in a newline and may contain internal newlines. When + a frame's source text spans multiple physical lines, only the first + physical line is displayed if column position information is + unavailable. For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact From a13acfac2fa14d935f8a1eb8d5d9465ed28ff99c Mon Sep 17 00:00:00 2001 From: harjoth Date: Thu, 9 Jul 2026 19:18:55 -0700 Subject: [PATCH 2/2] Document that multiline display also needs an end line number Column position information alone is not enough: end_lineno defaults to lineno, so a frame with colno/end_colno but no end_lineno still shows only its first physical line. Say so, and test it. --- Doc/library/traceback.rst | 13 ++++++++----- Lib/test/test_traceback.py | 11 +++++++++++ Lib/traceback.py | 14 ++++++++------ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 770ffb9814591c2..d4a531de8bb38bd 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -173,9 +173,10 @@ Module-Level Functions for printing. Each string in the resulting list corresponds to the item with the same index in the argument list. Each string ends in a newline and may contain internal newlines. When an item's source text spans multiple - physical lines, only the first physical line is displayed if column position - information is unavailable. Old-style tuples never have column position - information. + physical lines, every line is displayed only if the item has both column + position information and an end line number greater than its line number; + otherwise just the first physical line is displayed. Old-style tuples never + have column position information. .. function:: format_exception_only(exc, /[, value], *, show_group=False) @@ -484,8 +485,10 @@ the module-level functions described above. resulting list corresponds to a single :ref:`frame ` from the stack. Each string ends in a newline and may contain internal newlines. When a - frame's source text spans multiple physical lines, only the first physical - line is displayed if column position information is unavailable. + frame's source text spans multiple physical lines, every line is displayed + only if the frame has both column position information and an end line + number greater than its line number; otherwise just the first physical + line is displayed. For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 17c69768625718a..15e28b175856f60 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3438,6 +3438,17 @@ def test_from_list_multiline_with_columns(self): ' line 2\n'], s.format()) + def test_from_list_multiline_with_columns_without_end_lineno(self): + # end_lineno defaults to lineno, so the frame spans a single line + # and the trailing physical lines are not displayed. + frame = traceback.FrameSummary( + 'foo.py', 1, 'fred', line='line 1\nline 2', + colno=0, end_colno=6) + s = traceback.StackSummary.from_list([frame]) + self.assertEqual( + [' File "foo.py", line 1, in fred\n line 1\n'], + s.format()) + def test_format_list_multiline_old_style_tuple(self): frames = [ ('test.py', 10, 'func', diff --git a/Lib/traceback.py b/Lib/traceback.py index 5d9cf0936184d12..574fbaa3a4e176d 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -85,9 +85,10 @@ def format_list(extracted_list): Each string in the resulting list corresponds to the item with the same index in the argument list. Each string ends in a newline and may contain internal newlines. When an item's source text spans multiple - physical lines, only the first physical line is displayed if column - position information is unavailable. Old-style tuples never have column - position information. + physical lines, every line is displayed only if the item has both column + position information and an end line number greater than its line number; + otherwise just the first physical line is displayed. Old-style tuples + never have column position information. """ return StackSummary.from_list(extracted_list).format() @@ -802,9 +803,10 @@ def format(self, **kwargs): Returns a list of strings ready for printing. Each string in the resulting list corresponds to a single frame from the stack. Each string ends in a newline and may contain internal newlines. When - a frame's source text spans multiple physical lines, only the first - physical line is displayed if column position information is - unavailable. + a frame's source text spans multiple physical lines, every line is + displayed only if the frame has both column position information and an + end line number greater than its line number; otherwise just the first + physical line is displayed. For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact