From 634f3bec894878e4c22b77566118101ae3560e85 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 16 Jun 2025 08:01:04 +0000 Subject: [PATCH 01/13] add show_lines --- Lib/traceback.py | 129 ++++++++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 45 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index a1f175dbbaa421..614463825e542c 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -26,15 +26,18 @@ # -def print_list(extracted_list, file=None): +def print_list(extracted_list, file=None, *, show_lines=True): """Print the list of tuples as returned by extract_tb() or - extract_stack() as a formatted stack trace to the given file.""" + extract_stack() as a formatted stack trace to the given file. + + If show_lines is False, source code lines are not included in the output. + """ if file is None: file = sys.stderr - for item in StackSummary.from_list(extracted_list).format(): + for item in StackSummary.from_list(extracted_list).format(show_lines=show_lines): print(item, file=file, end="") -def format_list(extracted_list): +def format_list(extracted_list, *, show_lines=True): """Format a list of tuples or FrameSummary objects for printing. Given a list of tuples or FrameSummary objects as returned by @@ -45,26 +48,33 @@ def format_list(extracted_list): 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. + + If show_lines is False, source code lines are not included in the output. """ - return StackSummary.from_list(extracted_list).format() + return StackSummary.from_list(extracted_list).format(show_lines=show_lines) # # Printing and Extracting Tracebacks. # -def print_tb(tb, limit=None, file=None): +def print_tb(tb, limit=None, file=None, *, show_lines=True): """Print up to 'limit' stack trace entries from the traceback 'tb'. If 'limit' is omitted or None, all entries are printed. If 'file' is omitted or None, the output goes to sys.stderr; otherwise 'file' should be an open file or file-like object with a write() method. + + If show_lines is False, source code lines are not included in the output. """ - print_list(extract_tb(tb, limit=limit), file=file) + print_list(extract_tb(tb, limit=limit), file=file, show_lines=show_lines) + +def format_tb(tb, limit=None, *, show_lines=True): + """A shorthand for 'format_list(extract_tb(tb, limit))'. -def format_tb(tb, limit=None): - """A shorthand for 'format_list(extract_tb(tb, limit))'.""" - return extract_tb(tb, limit=limit).format() + If show_lines is False, source code lines are not included in the output. + """ + return extract_tb(tb, limit=limit).format(show_lines=show_lines) def extract_tb(tb, limit=None): """ @@ -80,7 +90,7 @@ def extract_tb(tb, limit=None): whitespace stripped; if the source is not available it is None. """ return StackSummary._extract_from_extended_frame_gen( - _walk_tb_with_full_positions(tb), limit=limit) + _walk_tb_with_full_positions(tb), limit=limit, lookup_lines=False) # # Exception formatting and output. @@ -117,7 +127,7 @@ def _parse_value_tb(exc, value, tb): def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ - file=None, chain=True, **kwargs): + file=None, chain=True, show_lines=True, **kwargs): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. This differs from print_tb() in the following ways: (1) if @@ -127,11 +137,14 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ appropriate format, it prints the line where the syntax error occurred with a caret on the next line indicating the approximate position of the error. + + If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) value, tb = _parse_value_tb(exc, value, tb) - te = TracebackException(type(value), value, tb, limit=limit, compact=True) - te.print(file=file, chain=chain, colorize=colorize) + te = TracebackException(type(value), value, tb, limit=limit, compact=True, + lookup_lines=show_lines) + te.print(file=file, chain=chain, colorize=colorize, show_lines=show_lines) BUILTIN_EXCEPTION_LIMIT = object() @@ -144,7 +157,7 @@ def _print_exception_bltin(exc, /): def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ - chain=True, **kwargs): + chain=True, show_lines=True, **kwargs): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -152,11 +165,14 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ ending in a newline and some containing internal newlines. When these lines are concatenated and printed, exactly the same text is printed as does print_exception(). + + If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) value, tb = _parse_value_tb(exc, value, tb) - te = TracebackException(type(value), value, tb, limit=limit, compact=True) - return list(te.format(chain=chain, colorize=colorize)) + te = TracebackException(type(value), value, tb, limit=limit, compact=True, + lookup_lines=show_lines) + return list(te.format(chain=chain, colorize=colorize, show_lines=show_lines)) def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs): @@ -205,47 +221,61 @@ def _safe_string(value, what, func=str): # -- -def print_exc(limit=None, file=None, chain=True): - """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'.""" - print_exception(sys.exception(), limit=limit, file=file, chain=chain) +def print_exc(limit=None, file=None, chain=True, *, show_lines=True): + """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'. + + If show_lines is False, source code lines are not included in the output. + """ + print_exception(sys.exception(), limit=limit, file=file, chain=chain, show_lines=show_lines) + +def format_exc(limit=None, chain=True, *, show_lines=True): + """Like print_exc() but return a string. -def format_exc(limit=None, chain=True): - """Like print_exc() but return a string.""" - return "".join(format_exception(sys.exception(), limit=limit, chain=chain)) + If show_lines is False, source code lines are not included in the output. + """ + return "".join(format_exception(sys.exception(), limit=limit, chain=chain, show_lines=show_lines)) + +def print_last(limit=None, file=None, chain=True, *, show_lines=True): + """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'. -def print_last(limit=None, file=None, chain=True): - """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'.""" + If show_lines is False, source code lines are not included in the output. + """ if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"): raise ValueError("no last exception") if hasattr(sys, "last_exc"): - print_exception(sys.last_exc, limit=limit, file=file, chain=chain) + print_exception(sys.last_exc, limit=limit, file=file, chain=chain, show_lines=show_lines) else: print_exception(sys.last_type, sys.last_value, sys.last_traceback, - limit=limit, file=file, chain=chain) + limit=limit, file=file, chain=chain, show_lines=show_lines) # # Printing and Extracting Stacks. # -def print_stack(f=None, limit=None, file=None): +def print_stack(f=None, limit=None, file=None, *, show_lines=True): """Print a stack trace from its invocation point. The optional 'f' argument can be used to specify an alternate stack frame at which to start. The optional 'limit' and 'file' arguments have the same meaning as for print_exception(). + + If show_lines is False, source code lines are not included in the output. """ if f is None: f = sys._getframe().f_back - print_list(extract_stack(f, limit=limit), file=file) + print_list(extract_stack(f, limit=limit), file=file, show_lines=show_lines) + +def format_stack(f=None, limit=None, *, show_lines=True): + """Shorthand for 'format_list(extract_stack(f, limit))'. -def format_stack(f=None, limit=None): - """Shorthand for 'format_list(extract_stack(f, limit))'.""" + If show_lines is False, source code lines are not included in the output. + """ if f is None: f = sys._getframe().f_back - return format_list(extract_stack(f, limit=limit)) + return format_list(extract_stack(f, limit=limit), show_lines=show_lines) def extract_stack(f=None, limit=None): @@ -259,7 +289,7 @@ def extract_stack(f=None, limit=None): """ if f is None: f = sys._getframe().f_back - stack = StackSummary.extract(walk_stack(f), limit=limit) + stack = StackSummary.extract(walk_stack(f), limit=limit, lookup_lines=False) stack.reverse() return stack @@ -436,7 +466,7 @@ class StackSummary(list): @classmethod 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. :param frame_gen: A generator that yields (frame, lineno) tuples @@ -525,11 +555,13 @@ def from_list(klass, a_list): result.append(FrameSummary(filename, lineno, name, line=line)) return result - def format_frame_summary(self, frame_summary, **kwargs): + def format_frame_summary(self, frame_summary, *, show_lines=True, **kwargs): """Format the lines for a single FrameSummary. Returns a string representing one frame involved in the stack. This gets called for every frame to be printed in the stack summary. + + If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) row = [] @@ -553,7 +585,7 @@ def format_frame_summary(self, frame_summary, **kwargs): theme.reset, ) ) - if frame_summary._dedented_lines and frame_summary._dedented_lines.strip(): + if show_lines and frame_summary._dedented_lines and frame_summary._dedented_lines.strip(): if ( frame_summary.colno is None or frame_summary.end_colno is None @@ -742,7 +774,7 @@ def _spawns_full_line(value): return True return False - def format(self, **kwargs): + def format(self, *, show_lines=True, **kwargs): """Format the stack ready for printing. Returns a list of strings ready for printing. Each string in the @@ -753,6 +785,8 @@ def format(self, **kwargs): For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact number of further repetitions. + + If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) result = [] @@ -761,7 +795,7 @@ def format(self, **kwargs): last_name = None count = 0 for frame_summary in self: - formatted_frame = self.format_frame_summary(frame_summary, colorize=colorize) + formatted_frame = self.format_frame_summary(frame_summary, show_lines=show_lines, colorize=colorize) if formatted_frame is None: continue if (last_file is None or last_file != frame_summary.filename or @@ -1465,7 +1499,7 @@ def _format_syntax_error(self, stype, **kwargs): filename_suffix, ) - def format(self, *, chain=True, _ctx=None, **kwargs): + def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): """Format the exception. If chain is not *True*, *__cause__* and *__context__* will not be formatted. @@ -1476,6 +1510,8 @@ def format(self, *, chain=True, _ctx=None, **kwargs): The message indicating which exception occurred is always the last string in the output. + + If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) if _ctx is None: @@ -1507,7 +1543,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs): if exc.exceptions is None: if exc.stack: yield from _ctx.emit('Traceback (most recent call last):\n') - yield from _ctx.emit(exc.stack.format(colorize=colorize)) + yield from _ctx.emit(exc.stack.format(show_lines=show_lines, colorize=colorize)) yield from _ctx.emit(exc.format_exception_only(colorize=colorize)) elif _ctx.exception_group_depth > self.max_group_depth: # exception group, but depth exceeds limit @@ -1523,7 +1559,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs): yield from _ctx.emit( 'Exception Group Traceback (most recent call last):\n', margin_char = '+' if is_toplevel else None) - yield from _ctx.emit(exc.stack.format(colorize=colorize)) + yield from _ctx.emit(exc.stack.format(show_lines=show_lines, colorize=colorize)) yield from _ctx.emit(exc.format_exception_only(colorize=colorize)) num_excs = len(exc.exceptions) @@ -1548,7 +1584,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs): f'+---------------- {title} ----------------\n') _ctx.exception_group_depth += 1 if not truncated: - yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx, colorize=colorize) + yield from exc.exceptions[i].format(chain=chain, show_lines=show_lines, _ctx=_ctx, colorize=colorize) else: remaining = num_excs - self.max_group_width plural = 's' if remaining > 1 else '' @@ -1566,12 +1602,15 @@ def format(self, *, chain=True, _ctx=None, **kwargs): _ctx.exception_group_depth = 0 - def print(self, *, file=None, chain=True, **kwargs): - """Print the result of self.format(chain=chain) to 'file'.""" + def print(self, *, file=None, chain=True, show_lines=True, **kwargs): + """Print the result of self.format(chain=chain) to 'file'. + + If show_lines is False, source code lines are not included in the output. + """ colorize = kwargs.get("colorize", False) if file is None: file = sys.stderr - for line in self.format(chain=chain, colorize=colorize): + for line in self.format(chain=chain, show_lines=show_lines, colorize=colorize): print(line, file=file, end="") From a0b4b704a620e610fac86010062d180ece4d5942 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 16 Jun 2025 10:21:44 +0000 Subject: [PATCH 02/13] add test --- Lib/test/test_traceback.py | 166 +++++++++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 74b979d009664d..a817068ad8b63a 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -564,12 +564,12 @@ def test_signatures(self): self.assertEqual( str(inspect.signature(traceback.print_exception)), ('(exc, /, value=, tb=, ' - 'limit=None, file=None, chain=True, **kwargs)')) + 'limit=None, file=None, chain=True, show_lines=True, **kwargs)')) self.assertEqual( str(inspect.signature(traceback.format_exception)), ('(exc, /, value=, tb=, limit=None, ' - 'chain=True, **kwargs)')) + 'chain=True, show_lines=True, **kwargs)')) self.assertEqual( str(inspect.signature(traceback.format_exception_only)), @@ -3340,7 +3340,7 @@ def some_inner(k, v): def test_custom_format_frame(self): class CustomStackSummary(traceback.StackSummary): - def format_frame_summary(self, frame_summary, colorize=False): + def format_frame_summary(self, frame_summary, **kwargs): return f'{frame_summary.filename}:{frame_summary.lineno}' def some_inner(): @@ -3365,10 +3365,10 @@ def g(): tb = g() class Skip_G(traceback.StackSummary): - def format_frame_summary(self, frame_summary, colorize=False): + def format_frame_summary(self, frame_summary, **kwargs): if frame_summary.name == 'g': return None - return super().format_frame_summary(frame_summary) + return super().format_frame_summary(frame_summary, **kwargs) stack = Skip_G.extract( traceback.walk_tb(tb)).format() @@ -4877,5 +4877,161 @@ def expected(t, m, fn, l, f, E, e, z): ] self.assertEqual(actual, expected(**colors)) + +class TestShowLines(unittest.TestCase): + """Tests for the show_lines parameter in traceback formatting functions.""" + + def setUp(self): + # Create a simple exception for testing + try: + x = 1 / 0 + except ZeroDivisionError as e: + self.exc = e + + def test_print_tb_show_lines_true(self): + """Test print_tb with show_lines=True (default)""" + output = StringIO() + traceback.print_tb(self.exc.__traceback__, file=output, show_lines=True) + result = output.getvalue() + self.assertIn('x = 1 / 0', result) + self.assertIn('File ', result) + + def test_print_tb_show_lines_false(self): + """Test print_tb with show_lines=False""" + output = StringIO() + traceback.print_tb(self.exc.__traceback__, file=output, show_lines=False) + result = output.getvalue() + self.assertNotIn('x = 1 / 0', result) + self.assertIn('File ', result) # File info should still be present + + def test_format_tb_show_lines_true(self): + """Test format_tb with show_lines=True (default)""" + result = traceback.format_tb(self.exc.__traceback__, show_lines=True) + formatted = ''.join(result) + self.assertIn('x = 1 / 0', formatted) + self.assertIn('File ', formatted) + + def test_format_tb_show_lines_false(self): + """Test format_tb with show_lines=False""" + result = traceback.format_tb(self.exc.__traceback__, show_lines=False) + formatted = ''.join(result) + self.assertNotIn('x = 1 / 0', formatted) + self.assertIn('File ', formatted) # File info should still be present + + def test_print_exception_show_lines_true(self): + """Test print_exception with show_lines=True (default)""" + output = StringIO() + traceback.print_exception(self.exc, file=output, show_lines=True) + result = output.getvalue() + self.assertIn('x = 1 / 0', result) + self.assertIn('ZeroDivisionError', result) + + def test_print_exception_show_lines_false(self): + """Test print_exception with show_lines=False""" + output = StringIO() + traceback.print_exception(self.exc, file=output, show_lines=False) + result = output.getvalue() + self.assertNotIn('x = 1 / 0', result) + self.assertIn('ZeroDivisionError', result) # Exception type should still be present + + def test_format_exception_show_lines_true(self): + """Test format_exception with show_lines=True (default)""" + result = traceback.format_exception(self.exc, show_lines=True) + formatted = ''.join(result) + self.assertIn('x = 1 / 0', formatted) + self.assertIn('ZeroDivisionError', formatted) + + def test_format_exception_show_lines_false(self): + """Test format_exception with show_lines=False""" + result = traceback.format_exception(self.exc, show_lines=False) + formatted = ''.join(result) + self.assertNotIn('x = 1 / 0', formatted) + self.assertIn('ZeroDivisionError', formatted) # Exception type should still be present + + def test_print_exc_show_lines_false(self): + """Test print_exc with show_lines=False""" + # Override sys.exception() to return our test exception + original_exception = sys.exception + sys.exception = lambda: self.exc + try: + output = StringIO() + traceback.print_exc(file=output, show_lines=False) + result = output.getvalue() + self.assertNotIn('x = 1 / 0', result) + self.assertIn('ZeroDivisionError', result) + finally: + sys.exception = original_exception + + def test_format_exc_show_lines_false(self): + """Test format_exc with show_lines=False""" + # Override sys.exception() to return our test exception + original_exception = sys.exception + sys.exception = lambda: self.exc + try: + result = traceback.format_exc(show_lines=False) + self.assertNotIn('x = 1 / 0', result) + self.assertIn('ZeroDivisionError', result) + finally: + sys.exception = original_exception + + def test_print_stack_show_lines_false(self): + """Test print_stack with show_lines=False""" + output = StringIO() + traceback.print_stack(file=output, show_lines=False) + result = output.getvalue() + # Should not contain source code lines + lines = result.split('\n') + # Filter out empty lines and check that remaining lines are just file/line info + non_empty_lines = [line for line in lines if line.strip()] + for line in non_empty_lines: + if line.strip(): + self.assertTrue(line.strip().startswith('File ') or + 'in ' in line or + line.strip() == 'traceback.print_stack(file=output, show_lines=False)') + + def test_format_stack_show_lines_false(self): + """Test format_stack with show_lines=False""" + result = traceback.format_stack(show_lines=False) + formatted = ''.join(result) + # Should contain file information but not source code + self.assertIn('File ', formatted) + # Check that the source code of this test is not included + self.assertNotIn('traceback.format_stack(show_lines=False)', formatted) + + def test_format_list_show_lines_false(self): + """Test format_list with show_lines=False""" + tb_list = traceback.extract_tb(self.exc.__traceback__) + result = traceback.format_list(tb_list, show_lines=False) + formatted = ''.join(result) + self.assertNotIn('x = 1 / 0', formatted) + self.assertIn('File ', formatted) # File info should still be present + + def test_print_list_show_lines_false(self): + """Test print_list with show_lines=False""" + tb_list = traceback.extract_tb(self.exc.__traceback__) + output = StringIO() + traceback.print_list(tb_list, file=output, show_lines=False) + result = output.getvalue() + self.assertNotIn('x = 1 / 0', result) + self.assertIn('File ', result) # File info should still be present + + def test_traceback_exception_show_lines_false(self): + """Test TracebackException with show_lines=False""" + te = traceback.TracebackException.from_exception(self.exc) + result = list(te.format(show_lines=False)) + formatted = ''.join(result) + self.assertNotIn('x = 1 / 0', formatted) + self.assertIn('ZeroDivisionError', formatted) + + def test_traceback_exception_print_show_lines_false(self): + """Test TracebackException.print with show_lines=False""" + te = traceback.TracebackException.from_exception(self.exc) + output = StringIO() + te.print(file=output, show_lines=False) + result = output.getvalue() + self.assertNotIn('x = 1 / 0', result) + self.assertIn('ZeroDivisionError', result) + + if __name__ == "__main__": unittest.main() From 979423ff3c5f4cc096e33046abed1d8f05e7e0b6 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 19 Jun 2025 07:57:49 +0000 Subject: [PATCH 03/13] add recent_first parameter to traceback functions --- Lib/test/test_traceback.py | 5 +- Lib/traceback.py | 179 ++++++++++++++++++++----------------- 2 files changed, 102 insertions(+), 82 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index a817068ad8b63a..f1b7c4d2ef4bcc 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -564,12 +564,13 @@ def test_signatures(self): self.assertEqual( str(inspect.signature(traceback.print_exception)), ('(exc, /, value=, tb=, ' - 'limit=None, file=None, chain=True, show_lines=True, **kwargs)')) + 'limit=None, file=None, chain=True, *, ' + 'show_lines=True, recent_first=False, **kwargs)')) self.assertEqual( str(inspect.signature(traceback.format_exception)), ('(exc, /, value=, tb=, limit=None, ' - 'chain=True, show_lines=True, **kwargs)')) + 'chain=True, *, show_lines=True, recent_first=False, **kwargs)')) self.assertEqual( str(inspect.signature(traceback.format_exception_only)), diff --git a/Lib/traceback.py b/Lib/traceback.py index 614463825e542c..fcdaddbc587b8b 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -30,11 +30,15 @@ def print_list(extracted_list, file=None, *, show_lines=True): """Print the list of tuples as returned by extract_tb() or extract_stack() as a formatted stack trace to the given file. - If show_lines is False, source code lines are not included in the output. + To print in "recent call first" order, call "extracted_list.reverse()" + before passing it to this function. + + If 'show_lines' is false, source code lines are not included in the output. """ if file is None: file = sys.stderr - for item in StackSummary.from_list(extracted_list).format(show_lines=show_lines): + for item in StackSummary.from_list(extracted_list).format( + show_lines=show_lines): print(item, file=file, end="") def format_list(extracted_list, *, show_lines=True): @@ -44,12 +48,10 @@ def format_list(extracted_list, *, show_lines=True): extract_tb() or 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. + Each string ends in a newline; the strings may contain internal newlines as + well, for those items whose source text line is not None. - If show_lines is False, source code lines are not included in the output. + If 'show_lines' is false, source code lines are not included in the output. """ return StackSummary.from_list(extracted_list).format(show_lines=show_lines) @@ -57,7 +59,7 @@ def format_list(extracted_list, *, show_lines=True): # Printing and Extracting Tracebacks. # -def print_tb(tb, limit=None, file=None, *, show_lines=True): +def print_tb(tb, limit=None, file=None, *, show_lines=True, recent_first=False): """Print up to 'limit' stack trace entries from the traceback 'tb'. If 'limit' is omitted or None, all entries are printed. If 'file' @@ -65,16 +67,22 @@ def print_tb(tb, limit=None, file=None, *, show_lines=True): 'file' should be an open file or file-like object with a write() method. - If show_lines is False, source code lines are not included in the output. + If 'show_lines' is false, source code lines are not included in the output. + If 'recent_first' is true, the stack trace is printed in "most recent call + first" order. """ - print_list(extract_tb(tb, limit=limit), file=file, show_lines=show_lines) + tblist = extract_tb(tb, limit=limit) + if recent_first: + tblist.reverse() + print_list(tblist, file=file, show_lines=show_lines) -def format_tb(tb, limit=None, *, show_lines=True): - """A shorthand for 'format_list(extract_tb(tb, limit))'. +def format_tb(tb, limit=None, *, show_lines=True, recent_first=False): + """A shorthand for 'format_list(extract_tb(tb, limit))'.""" - If show_lines is False, source code lines are not included in the output. - """ - return extract_tb(tb, limit=limit).format(show_lines=show_lines) + tblist = extract_tb(tb, limit=limit) + if recent_first: + tblist.reverse() + return tblist.format(show_lines=show_lines) def extract_tb(tb, limit=None): """ @@ -90,7 +98,7 @@ def extract_tb(tb, limit=None): whitespace stripped; if the source is not available it is None. """ return StackSummary._extract_from_extended_frame_gen( - _walk_tb_with_full_positions(tb), limit=limit, lookup_lines=False) + _walk_tb_with_full_positions(tb), limit=limit) # # Exception formatting and output. @@ -126,8 +134,9 @@ def _parse_value_tb(exc, value, tb): return value, tb -def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ - file=None, chain=True, show_lines=True, **kwargs): +def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, + file=None, chain=True, *, show_lines=True, + recent_first=False, **kwargs): """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. This differs from print_tb() in the following ways: (1) if @@ -138,13 +147,15 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ occurred with a caret on the next line indicating the approximate position of the error. - If show_lines is False, source code lines are not included in the output. + If 'show_lines' is false, source code lines are not included in the output. + If 'recent_first' is true, exception is printed first and traceback is shown + by "most recent call first" order. """ colorize = kwargs.get("colorize", False) value, tb = _parse_value_tb(exc, value, tb) - te = TracebackException(type(value), value, tb, limit=limit, compact=True, - lookup_lines=show_lines) - te.print(file=file, chain=chain, colorize=colorize, show_lines=show_lines) + te = TracebackException(type(value), value, tb, limit=limit, compact=True) + te.print(file=file, chain=chain, colorize=colorize, + show_lines=show_lines, recent_first=recent_first) BUILTIN_EXCEPTION_LIMIT = object() @@ -156,8 +167,8 @@ def _print_exception_bltin(exc, /): return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file, colorize=colorize) -def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ - chain=True, show_lines=True, **kwargs): +def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, + chain=True, *, show_lines=True, recent_first=False, **kwargs): """Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments @@ -165,14 +176,12 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ ending in a newline and some containing internal newlines. When these lines are concatenated and printed, exactly the same text is printed as does print_exception(). - - If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) value, tb = _parse_value_tb(exc, value, tb) - te = TracebackException(type(value), value, tb, limit=limit, compact=True, - lookup_lines=show_lines) - return list(te.format(chain=chain, colorize=colorize, show_lines=show_lines)) + te = TracebackException(type(value), value, tb, limit=limit, compact=True) + return list(te.format(chain=chain, colorize=colorize, + show_lines=show_lines, recent_first=recent_first)) def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs): @@ -221,61 +230,63 @@ def _safe_string(value, what, func=str): # -- -def print_exc(limit=None, file=None, chain=True, *, show_lines=True): - """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain)'. - - If show_lines is False, source code lines are not included in the output. - """ - print_exception(sys.exception(), limit=limit, file=file, chain=chain, show_lines=show_lines) - -def format_exc(limit=None, chain=True, *, show_lines=True): - """Like print_exc() but return a string. - - If show_lines is False, source code lines are not included in the output. - """ - return "".join(format_exception(sys.exception(), limit=limit, chain=chain, show_lines=show_lines)) - -def print_last(limit=None, file=None, chain=True, *, show_lines=True): - """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain)'. - - If show_lines is False, source code lines are not included in the output. - """ +def print_exc(limit=None, file=None, chain=True, *, show_lines=True, + recent_first=False): + """Shorthand for 'print_exception(sys.exception(), limit=limit, file=file, chain=chain, ...)'.""" + print_exception(sys.exception(), limit=limit, file=file, chain=chain, + show_lines=show_lines, recent_first=recent_first) + +def format_exc(limit=None, chain=True, *, show_lines=True, recent_first=False): + """Like print_exc() but return a string.""" + return "".join(format_exception( + sys.exception(), limit=limit, chain=chain, + show_lines=show_lines, recent_first=recent_first)) + +def print_last(limit=None, file=None, chain=True, *, show_lines=True, + recent_first=False): + """This is a shorthand for 'print_exception(sys.last_exc, limit=limit, file=file, chain=chain, ...)'.""" if not hasattr(sys, "last_exc") and not hasattr(sys, "last_type"): raise ValueError("no last exception") if hasattr(sys, "last_exc"): - print_exception(sys.last_exc, limit=limit, file=file, chain=chain, show_lines=show_lines) + print_exception(sys.last_exc, limit=limit, file=file, chain=chain, + show_lines=show_lines, recent_first=recent_first) else: print_exception(sys.last_type, sys.last_value, sys.last_traceback, - limit=limit, file=file, chain=chain, show_lines=show_lines) + limit=limit, file=file, chain=chain, + show_lines=show_lines, recent_first=recent_first) # # Printing and Extracting Stacks. # -def print_stack(f=None, limit=None, file=None, *, show_lines=True): +def print_stack(f=None, limit=None, file=None, *, show_lines=True, recent_first=False): """Print a stack trace from its invocation point. The optional 'f' argument can be used to specify an alternate stack frame at which to start. The optional 'limit' and 'file' arguments have the same meaning as for print_exception(). - If show_lines is False, source code lines are not included in the output. + If 'show_lines' is false, source code lines are not included in the output. + If 'recent_first' is true, stack is printed by "most recent call first" order. """ if f is None: f = sys._getframe().f_back - print_list(extract_stack(f, limit=limit), file=file, show_lines=show_lines) + stack = extract_stack(f, limit=limit) + if recent_first: + stack.reverse() + print_list(stack, file=file, show_lines=show_lines) -def format_stack(f=None, limit=None, *, show_lines=True): - """Shorthand for 'format_list(extract_stack(f, limit))'. - - If show_lines is False, source code lines are not included in the output. - """ +def format_stack(f=None, limit=None, *, show_lines=True, recent_first=False): + """Shorthand for 'format_list(extract_stack(f, limit), show_lines)'.""" if f is None: f = sys._getframe().f_back - return format_list(extract_stack(f, limit=limit), show_lines=show_lines) + stack = extract_stack(f, limit=limit) + if recent_first: + stack.reverse() + return format_list(stack, show_lines=show_lines) def extract_stack(f=None, limit=None): @@ -289,7 +300,8 @@ def extract_stack(f=None, limit=None): """ if f is None: f = sys._getframe().f_back - stack = StackSummary.extract(walk_stack(f), limit=limit, lookup_lines=False) + stack = StackSummary.extract(walk_stack(f), limit=limit) + # Traceback should use "recent call last" order. stack.reverse() return stack @@ -465,7 +477,7 @@ class StackSummary(list): """A list of FrameSummary objects, representing a stack of frames.""" @classmethod - def extract(klass, frame_gen, *, limit=None, lookup_lines=True, + def extract(klass, frame_gen, *, limit=None, lookup_lines=False, capture_locals=False): """Create a StackSummary from a traceback or stack object. @@ -488,7 +500,7 @@ def extended_frame_gen(): @classmethod def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None, - lookup_lines=True, capture_locals=False): + lookup_lines=False, capture_locals=False): # Same as extract but operates on a frame generator that yields # (frame, (lineno, end_lineno, colno, end_colno)) in the stack. # Only lineno is required, the remaining fields can be None if the @@ -560,8 +572,6 @@ def format_frame_summary(self, frame_summary, *, show_lines=True, **kwargs): Returns a string representing one frame involved in the stack. This gets called for every frame to be printed in the stack summary. - - If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) row = [] @@ -785,8 +795,6 @@ def format(self, *, show_lines=True, **kwargs): For long sequences of the same frame and line, the first few repetitions are shown, followed by a summary line stating the exact number of further repetitions. - - If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) result = [] @@ -1076,7 +1084,7 @@ class TracebackException: """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, compact=False, + lookup_lines=False, capture_locals=False, compact=False, max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we @@ -1499,7 +1507,7 @@ def _format_syntax_error(self, stype, **kwargs): filename_suffix, ) - def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): + def format(self, *, chain=True, show_lines=True, _ctx=None, recent_first=False, **kwargs): """Format the exception. If chain is not *True*, *__cause__* and *__context__* will not be formatted. @@ -1510,8 +1518,6 @@ def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): The message indicating which exception occurred is always the last string in the output. - - If show_lines is False, source code lines are not included in the output. """ colorize = kwargs.get("colorize", False) if _ctx is None: @@ -1541,10 +1547,16 @@ def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): if msg is not None: yield from _ctx.emit(msg) if exc.exceptions is None: - if exc.stack: + if not recent_first and exc.stack: yield from _ctx.emit('Traceback (most recent call last):\n') - yield from _ctx.emit(exc.stack.format(show_lines=show_lines, colorize=colorize)) + yield from _ctx.emit(exc.stack.format( + show_lines=show_lines, colorize=colorize)) yield from _ctx.emit(exc.format_exception_only(colorize=colorize)) + if recent_first and exc.stack: + yield from _ctx.emit('Traceback (most recent call first):\n') + reversed_stack = StackSummary(reversed(self.stack)) + yield from _ctx.emit(reversed_stack.format( + show_lines=show_lines, colorize=colorize)) elif _ctx.exception_group_depth > self.max_group_depth: # exception group, but depth exceeds limit yield from _ctx.emit( @@ -1555,13 +1567,22 @@ def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): if is_toplevel: _ctx.exception_group_depth += 1 - if exc.stack: + if not recent_first and exc.stack: yield from _ctx.emit( 'Exception Group Traceback (most recent call last):\n', margin_char = '+' if is_toplevel else None) - yield from _ctx.emit(exc.stack.format(show_lines=show_lines, colorize=colorize)) + yield from _ctx.emit(exc.stack.format( + show_lines=show_lines, colorize=colorize)) yield from _ctx.emit(exc.format_exception_only(colorize=colorize)) + if recent_first and exc.stack: + yield from _ctx.emit( + 'Exception Group Traceback (most recent call first):\n', + margin_char = '+' if is_toplevel else None) + reversed_stack = StackSummary(reversed(self.stack)) + yield from _ctx.emit(reversed_stack.format( + show_lines=show_lines, colorize=colorize)) + num_excs = len(exc.exceptions) if num_excs <= self.max_group_width: n = num_excs @@ -1602,15 +1623,13 @@ def format(self, *, chain=True, show_lines=True, _ctx=None, **kwargs): _ctx.exception_group_depth = 0 - def print(self, *, file=None, chain=True, show_lines=True, **kwargs): - """Print the result of self.format(chain=chain) to 'file'. - - If show_lines is False, source code lines are not included in the output. - """ + def print(self, *, file=None, chain=True, show_lines=True, recent_first=False, **kwargs): + """Print the result of self.format(chain=chain) to 'file'.""" colorize = kwargs.get("colorize", False) if file is None: file = sys.stderr - for line in self.format(chain=chain, show_lines=show_lines, colorize=colorize): + for line in self.format(chain=chain, show_lines=show_lines, + recent_first=recent_first, colorize=colorize): print(line, file=file, end="") From 6038d11d7e2eb17757a4b8d4680466e9fcbb38e8 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 20 Jun 2025 18:24:24 +0900 Subject: [PATCH 04/13] add test for recent_first --- Lib/test/test_traceback.py | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index f1b7c4d2ef4bcc..5117c7ed959f4f 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -5034,5 +5034,89 @@ def test_traceback_exception_print_show_lines_false(self): self.assertIn('ZeroDivisionError', result) +class TestRecentFirst(unittest.TestCase): + """Tests for the recent_first parameter in traceback formatting functions.""" + + def setUp(self): + # Create a simple exception for testing + def f1(): + return 1 / 0 + + def f2(): + return f1() + + try: + f2() + except ZeroDivisionError as e: + self.exc = e + + def test_print_tb_recent_first(self): + """Test print_tb with recent_first=True""" + output = StringIO() + traceback.print_tb(self.exc.__traceback__, file=output, recent_first=True) + result = output.getvalue() + f1pos = result.index(", in f1") + f2pos = result.index(", in f2") + self.assertLess(f1pos, f2pos, "f1 should be printed before f2") + + def test_format_tb_recent_first(self): + """Test format_tb with recent_first=True""" + result = traceback.format_tb(self.exc.__traceback__, recent_first=True) + formatted = ''.join(result) + f1pos = formatted.index(", in f1") + f2pos = formatted.index(", in f2") + self.assertLess(f1pos, f2pos, "f1 should be printed before f2") + + def check_recent_first_exception_order(self, result: str): + """Helper to check if the recent_first order is correct in the result.""" + lines = result.splitlines() + self.assertEqual(lines[0], "ZeroDivisionError: division by zero") + self.assertEqual(lines[1], "Traceback (most recent call first):") + + f1pos = result.index(", in f1") + f2pos = result.index(", in f2") + self.assertLess(f1pos, f2pos, "f1 should be printed before f2") + + def test_print_exception_recent_first(self): + """Test print_exception with recent_first=True""" + output = StringIO() + traceback.print_exception(self.exc, file=output, recent_first=True) + self.check_recent_first_exception_order(output.getvalue()) + + def test_format_exception_recent_first(self): + """Test format_exception with recent_first=True""" + result = traceback.format_exception(self.exc, recent_first=True) + self.check_recent_first_exception_order(''.join(result)) + + def test_print_stack_recent_first(self): + """Test print_stack with recent_first=True""" + output = StringIO() + + def f1(): + traceback.print_stack(file=output, recent_first=True) + + def f2(): + f1() + + f2() + result = output.getvalue() + f1pos = result.index(", in f1") + f2pos = result.index(", in f2") + self.assertLess(f1pos, f2pos, "f1 should be printed before f2") + + def test_format_stack_recent_first(self): + """Test format_stack with recent_first=True""" + def f1(): + return traceback.format_stack(recent_first=True) + + def f2(): + return f1() + + result = ''.join(f2()) + f1pos = result.index(", in f1") + f2pos = result.index(", in f2") + self.assertLess(f1pos, f2pos, "f1 should be printed before f2") + + if __name__ == "__main__": unittest.main() From ded22f3fa3fb5ee75712972e2b4759bb99712f21 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jun 2025 02:29:58 +0000 Subject: [PATCH 05/13] add doc --- Doc/library/traceback.rst | 148 +++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 24 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index b5464ac55ddfa9..3c6fc7abb0beb7 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -52,7 +52,7 @@ The module's API can be divided into two parts: Module-Level Functions ---------------------- -.. function:: print_tb(tb, limit=None, file=None) +.. function:: print_tb(tb, limit=None, file=None, *, show_lines=True, recent_first=False) Print up to *limit* stack trace entries from :ref:`traceback object ` *tb* (starting @@ -63,6 +63,19 @@ Module-Level Functions :term:`file ` or :term:`file-like object` to receive the output. + If *show_lines* is false, source code lines are not included in the output. + + If *recent_first* is true, the most recent stack trace entries are printed + first, otherwise the oldest entries are printed first. The default is false. + + .. note:: + ``recent_first=True`` is useful for showing stack traces in places where + people see the top of the stack trace first, such as in a web browser. + + ``recent_first=False`` is useful for showing stack traces in places where + people see the bottom of the stack trace first, such as a console or log + files watched with :command:`tail -f`. + .. note:: The meaning of the *limit* parameter is different than the meaning @@ -74,9 +87,12 @@ Module-Level Functions .. versionchanged:: 3.5 Added negative *limit* support. + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. + .. function:: print_exception(exc, /[, value, tb], limit=None, \ - file=None, chain=True) + file=None, chain=True, *, show_lines=True, recent_first=False) Print exception information and stack trace entries from :ref:`traceback object ` @@ -103,7 +119,13 @@ Module-Level Functions :attr:`~BaseException.__cause__` or :attr:`~BaseException.__context__` attributes of the exception) will be printed as well, like the interpreter itself does when printing an unhandled - exception. + exception. If *show_lines* is ``False``, source code lines are not included + in the output. + + If *show_lines* is false, source code lines are not included in the output. + + If *recent_first* is true, the most recent stack trace entries are printed + first, otherwise the oldest entries are printed first. The default is false. .. versionchanged:: 3.5 The *etype* argument is ignored and inferred from the type of *value*. @@ -112,21 +134,31 @@ Module-Level Functions The *etype* parameter has been renamed to *exc* and is now positional-only. + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. -.. function:: print_exc(limit=None, file=None, chain=True) + +.. function:: print_exc(limit=None, file=None, chain=True, *, show_lines=True, recent_first=False) This is a shorthand for ``print_exception(sys.exception(), limit=limit, file=file, - chain=chain)``. + chain=chain, show_lines=show_lines, recent_first=recent_first)``. + + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. -.. function:: print_last(limit=None, file=None, chain=True) +.. function:: print_last(limit=None, file=None, chain=True, *, show_lines=True, recent_first=False) This is a shorthand for ``print_exception(sys.last_exc, limit=limit, file=file, - chain=chain)``. In general it will work only after an exception has reached - an interactive prompt (see :data:`sys.last_exc`). + chain=chain, show_lines=show_lines, recent_first=recent_first)``. + In general it will work only after an exception has reached an interactive + prompt (see :data:`sys.last_exc`). + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. -.. function:: print_stack(f=None, limit=None, file=None) + +.. function:: print_stack(f=None, limit=None, file=None, *, show_lines=True, recent_first=False) Print up to *limit* stack trace entries (starting from the invocation point) if *limit* is positive. Otherwise, print the last ``abs(limit)`` @@ -134,10 +166,14 @@ Module-Level Functions The optional *f* argument can be used to specify an alternate :ref:`stack frame ` to start. The optional *file* argument has the same meaning as for - :func:`print_tb`. + :func:`print_tb`. If *show_lines* is ``False``, source code lines are + not included in the output. .. versionchanged:: 3.5 - Added negative *limit* support. + Added negative *limit* support. + + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. .. function:: extract_tb(tb, limit=None) @@ -161,21 +197,29 @@ Module-Level Functions arguments have the same meaning as for :func:`print_stack`. -.. function:: print_list(extracted_list, file=None) +.. function:: print_list(extracted_list, file=None, *, show_lines=True) Print the list of tuples as returned by :func:`extract_tb` or :func:`extract_stack` as a formatted stack trace to the given file. If *file* is ``None``, the output is written to :data:`sys.stderr`. + If *show_lines* is ``False``, source code lines are not included in the output. + .. versionchanged:: next + Added *show_lines* parameter. -.. function:: format_list(extracted_list) + +.. function:: format_list(extracted_list, *, show_lines=True) 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``. + text line is not ``None``. If *show_lines* is ``False``, source code lines + are not included in the output. + + .. versionchanged:: next + Added *show_lines* parameter. .. function:: format_exception_only(exc, /[, value], *, show_group=False) @@ -208,7 +252,7 @@ Module-Level Functions *show_group* parameter was added. -.. function:: format_exception(exc, /[, value, tb], limit=None, chain=True) +.. function:: format_exception(exc, /[, value, tb], limit=None, chain=True, *, show_lines=True, recent_first=False, show_group=False) Format a stack trace and the exception information. The arguments have the same meaning as the corresponding arguments to :func:`print_exception`. The @@ -216,6 +260,10 @@ Module-Level Functions containing internal newlines. When these lines are concatenated and printed, exactly the same text is printed as does :func:`print_exception`. + If *show_lines* is false, source code lines are not included in the output. + If *recent_first* is true, the most recent stack trace entries are printed + first, otherwise the oldest entries are printed first. The default is false. + .. versionchanged:: 3.5 The *etype* argument is ignored and inferred from the type of *value*. @@ -223,21 +271,41 @@ Module-Level Functions This function's behavior and signature were modified to match :func:`print_exception`. + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. + -.. function:: format_exc(limit=None, chain=True) +.. function:: format_exc(limit=None, chain=True, *, show_lines=True, recent_first=False) This is like ``print_exc(limit)`` but returns a string instead of printing to a file. + If *show_lines* is false, source code lines are not included in the output. + If *recent_first* is true, the most recent stack trace entries are printed + first, otherwise the oldest entries are printed first. The default is false. -.. function:: format_tb(tb, limit=None) + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. - A shorthand for ``format_list(extract_tb(tb, limit))``. +.. function:: format_tb(tb, limit=None, *, show_lines=True, recent_first=False) -.. function:: format_stack(f=None, limit=None) + A shorthand for ``format_list(extract_tb(tb, limit), show_lines=show_lines)``. - A shorthand for ``format_list(extract_stack(f, limit))``. + If *recent_first* is true, ``reversed(extract_tb(tb, limit))`` is used. + + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. + + +.. function:: format_stack(f=None, limit=None, *, show_lines=True, recent_first=False) + + A shorthand for ``format_list(extract_stack(f, limit), show_lines=show_lines)``. + + If *recent_first* is true, ``reversed(extract_stack(f, limit))`` is used. + + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. .. function:: clear_frames(tb) @@ -398,14 +466,24 @@ the module-level functions described above. Note that when locals are captured, they are also shown in the traceback. - .. method:: print(*, file=None, chain=True) + .. method:: print(*, file=None, chain=True, show_lines=True, recent_first=False) Print to *file* (default ``sys.stderr``) the exception information returned by :meth:`format`. + If *show_lines* is false, source code lines from being included in the output. + + If *recent_first* is true, the exception is printed first followed by stack + trace by "most recent call first" order. + Otherwise, the stack trace is printed first by "most recent call last" order + followed by the exception. + .. versionadded:: 3.11 - .. method:: format(*, chain=True) + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. + + .. method:: format(*, chain=True, show_lines=True, recent_first=False, **kwargs) Format the exception. @@ -416,6 +494,16 @@ the module-level functions described above. some containing internal newlines. :func:`~traceback.print_exception` is a wrapper around this method which just prints the lines to a file. + If *show_lines* is false, source code lines from being included in the output. + + If *recent_first* is true, the exception is printed first followed by stack + trace by "most recent call first" order. + Otherwise, the stack trace is printed first by "most recent call last" order + followed by the exception. + + .. versionchanged:: next + Added *show_lines* and *recent_first* parameters. + .. method:: format_exception_only(*, show_group=False) Format the exception part of the traceback. @@ -474,7 +562,7 @@ the module-level functions described above. should be a 4-tuple with *filename*, *lineno*, *name*, *line* as the elements. - .. method:: format() + .. method:: format(*, show_lines=True) Returns a list of strings ready for printing. Each string in the resulting list corresponds to a single :ref:`frame ` from @@ -486,10 +574,16 @@ the module-level functions described above. repetitions are shown, followed by a summary line stating the exact number of further repetitions. + The keyword argument *show_lines*, if ``False``, prevents source code + lines from being included in the output. + .. versionchanged:: 3.6 Long sequences of repeated frames are now abbreviated. - .. method:: format_frame_summary(frame_summary) + .. versionchanged:: next + Added the *show_lines* parameter. + + .. method:: format_frame_summary(frame_summary, *, show_lines=True, **kwargs) Returns a string for printing one of the :ref:`frames ` involved in the stack. @@ -497,8 +591,14 @@ the module-level functions described above. printed by :meth:`StackSummary.format`. If it returns ``None``, the frame is omitted from the output. + The keyword argument *show_lines*, if ``False``, prevents source code + lines from being included in the output. + .. versionadded:: 3.11 + .. versionchanged:: next + Added the *show_lines* parameter. + :class:`!FrameSummary` Objects ------------------------------ From b7454fb8f7ab72b39028d8923fcc66baa7051ee4 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 17 Jun 2025 02:39:19 +0000 Subject: [PATCH 06/13] add whatsnew entry --- Doc/whatsnew/3.15.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..d07e7f5a8dd51e 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -171,6 +171,15 @@ tarfile and :cve:`2025-4435`.) +traceback +---------- + +* The keyword only argument ``show_lines=True`` is added to all functions and + methods formatting or printing traceback. You can suppress the display of + source code lines in the traceback by passing ``show_lines=False``. + (Contributed by Inada Naoki in TBD). + + zlib ---- From 2efd9fb0eec1761fc895a9927a52a6352eb13c4e Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 20 Jun 2025 21:27:51 +0900 Subject: [PATCH 07/13] add whats'new entry and news entry --- Doc/whatsnew/3.15.rst | 14 ++++++++++---- .../2025-06-20-21-16-32.gh-issue-135751.W0f1C1.rst | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-20-21-16-32.gh-issue-135751.W0f1C1.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d07e7f5a8dd51e..2cf389027b6a81 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -174,10 +174,16 @@ tarfile traceback ---------- -* The keyword only argument ``show_lines=True`` is added to all functions and - methods formatting or printing traceback. You can suppress the display of - source code lines in the traceback by passing ``show_lines=False``. - (Contributed by Inada Naoki in TBD). +* Add new ``show_lines`` and ``recent_first`` keyword only arguments to + the :mod:`traceback` functions. + + The ``show_lines`` argument controls whether source code lines are displayed. + It is default to ``True``. + + The ``recent_first`` argument controls whether the most recent frames are + displayed first or last in the traceback. It affects wheher the exception + is displayed at the top or bottom of the traceback. It is default to ``False``. + (Contributed by Inada Naoki in :gh:`135751`) zlib diff --git a/Misc/NEWS.d/next/Library/2025-06-20-21-16-32.gh-issue-135751.W0f1C1.rst b/Misc/NEWS.d/next/Library/2025-06-20-21-16-32.gh-issue-135751.W0f1C1.rst new file mode 100644 index 00000000000000..06236dd0bcc1e8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-20-21-16-32.gh-issue-135751.W0f1C1.rst @@ -0,0 +1,2 @@ +Add *show_lines* and *recent_first* parameters to APIs in :mod:`traceback`. +Contributed by Inada Naoki. From fc0bf9b54a24ff88631b7b58b0555bb44d766a0b Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 20 Jun 2025 21:33:28 +0900 Subject: [PATCH 08/13] fix doc --- Doc/library/traceback.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 3c6fc7abb0beb7..ffa30532e6d102 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -351,7 +351,7 @@ storing this information by avoiding holding references to In addition, they expose more options to configure the output compared to the module-level functions described above. -.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False, max_group_width=15, max_group_depth=10) +.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=False, capture_locals=False, compact=False, max_group_width=15, max_group_depth=10) Capture an exception for later rendering. The meaning of *limit*, *lookup_lines* and *capture_locals* are as for the :class:`StackSummary` @@ -377,6 +377,10 @@ the module-level functions described above. .. versionchanged:: 3.11 Added the *max_group_width* and *max_group_depth* parameters. + .. versionchanged:: next + Changed *lookup_lines* default to ``False`` to avoid overhead when + formatting exceptions with ``show_lines=False``. + .. attribute:: __cause__ A :class:`!TracebackException` of the original @@ -459,13 +463,17 @@ the module-level functions described above. For syntax errors - the compiler error message. - .. classmethod:: from_exception(exc, *, limit=None, lookup_lines=True, capture_locals=False) + .. classmethod:: from_exception(exc, *, limit=None, lookup_lines=False, capture_locals=False) Capture an exception for later rendering. *limit*, *lookup_lines* and *capture_locals* are as for the :class:`StackSummary` class. Note that when locals are captured, they are also shown in the traceback. + .. versionchanged:: next + Changed *lookup_lines* default to ``False`` to avoid overhead when + formatting exceptions with ``show_lines=False``. + .. method:: print(*, file=None, chain=True, show_lines=True, recent_first=False) Print to *file* (default ``sys.stderr``) the exception information returned by @@ -537,7 +545,7 @@ the module-level functions described above. .. class:: StackSummary - .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False) + .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=False, capture_locals=False) Construct a :class:`!StackSummary` object from a frame generator (such as is returned by :func:`~traceback.walk_stack` or @@ -555,6 +563,10 @@ the module-level functions described above. Exceptions raised from :func:`repr` on a local variable (when *capture_locals* is ``True``) are no longer propagated to the caller. + .. versionchanged:: next + Changed *lookup_lines* default to ``False`` to avoid overhead when + formatting traceback with ``show_lines=False``. + .. classmethod:: from_list(a_list) Construct a :class:`!StackSummary` object from a supplied list of From 878ea1d9d819e68fc2664a3e686b2df2cfa041a8 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 20 Jun 2025 21:37:02 +0900 Subject: [PATCH 09/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Doc/library/traceback.rst | 3 +-- Doc/whatsnew/3.15.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index ffa30532e6d102..39201e5d8ba112 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -119,8 +119,7 @@ Module-Level Functions :attr:`~BaseException.__cause__` or :attr:`~BaseException.__context__` attributes of the exception) will be printed as well, like the interpreter itself does when printing an unhandled - exception. If *show_lines* is ``False``, source code lines are not included - in the output. + exception. If *show_lines* is false, source code lines are not included in the output. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 2cf389027b6a81..9298551550312f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -181,7 +181,7 @@ traceback It is default to ``True``. The ``recent_first`` argument controls whether the most recent frames are - displayed first or last in the traceback. It affects wheher the exception + displayed first or last in the traceback. It affects whether the exception is displayed at the top or bottom of the traceback. It is default to ``False``. (Contributed by Inada Naoki in :gh:`135751`) From 78dcf3553396b6c206bfef3913032c0c36066bda Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 20 Jun 2025 21:48:28 +0900 Subject: [PATCH 10/13] remove unrelated change --- Doc/library/traceback.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 39201e5d8ba112..aade7bbdae6fc8 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -490,7 +490,7 @@ the module-level functions described above. .. versionchanged:: next Added *show_lines* and *recent_first* parameters. - .. method:: format(*, chain=True, show_lines=True, recent_first=False, **kwargs) + .. method:: format(*, chain=True, show_lines=True, recent_first=False) Format the exception. From abaa5985819db03fd8ffea39bd4eeeebdb7c9923 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 21 Jun 2025 08:51:02 +0900 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/traceback.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/traceback.py b/Lib/traceback.py index fcdaddbc587b8b..f2f188d60d51b9 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -33,7 +33,7 @@ def print_list(extracted_list, file=None, *, show_lines=True): To print in "recent call first" order, call "extracted_list.reverse()" before passing it to this function. - If 'show_lines' is false, source code lines are not included in the output. + If 'show_lines' is true, source code lines are included in the output. """ if file is None: file = sys.stderr @@ -51,7 +51,7 @@ def format_list(extracted_list, *, show_lines=True): Each string ends in a newline; the strings may contain internal newlines as well, for those items whose source text line is not None. - If 'show_lines' is false, source code lines are not included in the output. + If 'show_lines' is true, source code lines are included in the output. """ return StackSummary.from_list(extracted_list).format(show_lines=show_lines) @@ -67,7 +67,7 @@ def print_tb(tb, limit=None, file=None, *, show_lines=True, recent_first=False): 'file' should be an open file or file-like object with a write() method. - If 'show_lines' is false, source code lines are not included in the output. + If 'show_lines' is true, source code lines are included in the output. If 'recent_first' is true, the stack trace is printed in "most recent call first" order. """ @@ -147,7 +147,7 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, occurred with a caret on the next line indicating the approximate position of the error. - If 'show_lines' is false, source code lines are not included in the output. + If 'show_lines' is true, source code lines are included in the output. If 'recent_first' is true, exception is printed first and traceback is shown by "most recent call first" order. """ @@ -268,7 +268,7 @@ def print_stack(f=None, limit=None, file=None, *, show_lines=True, recent_first= stack frame at which to start. The optional 'limit' and 'file' arguments have the same meaning as for print_exception(). - If 'show_lines' is false, source code lines are not included in the output. + If 'show_lines' is true, source code lines are included in the output. If 'recent_first' is true, stack is printed by "most recent call first" order. """ if f is None: From 6bb5450ad53895af17d6243a688efbb5c7b0c142 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 21 Jun 2025 08:57:44 +0900 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: Brian Schubert Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/library/traceback.rst | 29 ++++++++++++++--------------- Doc/whatsnew/3.15.rst | 6 +++--- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index aade7bbdae6fc8..379ed42b4b51ae 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -63,7 +63,7 @@ Module-Level Functions :term:`file ` or :term:`file-like object` to receive the output. - If *show_lines* is false, source code lines are not included in the output. + If *show_lines* is true (the default), source code lines will be included in the output. If *recent_first* is true, the most recent stack trace entries are printed first, otherwise the oldest entries are printed first. The default is false. @@ -121,7 +121,7 @@ Module-Level Functions printed as well, like the interpreter itself does when printing an unhandled exception. - If *show_lines* is false, source code lines are not included in the output. + If *show_lines* is true, source code lines are included in the output. If *recent_first* is true, the most recent stack trace entries are printed first, otherwise the oldest entries are printed first. The default is false. @@ -165,8 +165,8 @@ Module-Level Functions The optional *f* argument can be used to specify an alternate :ref:`stack frame ` to start. The optional *file* argument has the same meaning as for - :func:`print_tb`. If *show_lines* is ``False``, source code lines are - not included in the output. + :func:`print_tb`. If *show_lines* is true, source code lines are + included in the output. .. versionchanged:: 3.5 Added negative *limit* support. @@ -201,7 +201,7 @@ Module-Level Functions Print the list of tuples as returned by :func:`extract_tb` or :func:`extract_stack` as a formatted stack trace to the given file. If *file* is ``None``, the output is written to :data:`sys.stderr`. - If *show_lines* is ``False``, source code lines are not included in the output. + If *show_lines* is true, source code lines are included in the output. .. versionchanged:: next Added *show_lines* parameter. @@ -214,8 +214,8 @@ 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; the strings may contain internal newlines as well, for those items whose source - text line is not ``None``. If *show_lines* is ``False``, source code lines - are not included in the output. + text line is not ``None``. If *show_lines* is ``True``, source code lines + are included in the output. .. versionchanged:: next Added *show_lines* parameter. @@ -259,7 +259,7 @@ Module-Level Functions containing internal newlines. When these lines are concatenated and printed, exactly the same text is printed as does :func:`print_exception`. - If *show_lines* is false, source code lines are not included in the output. + If *show_lines* is true, source code lines are included in the output. If *recent_first* is true, the most recent stack trace entries are printed first, otherwise the oldest entries are printed first. The default is false. @@ -279,7 +279,7 @@ Module-Level Functions This is like ``print_exc(limit)`` but returns a string instead of printing to a file. - If *show_lines* is false, source code lines are not included in the output. + If *show_lines* is true, source code lines are included in the output. If *recent_first* is true, the most recent stack trace entries are printed first, otherwise the oldest entries are printed first. The default is false. @@ -478,7 +478,7 @@ the module-level functions described above. Print to *file* (default ``sys.stderr``) the exception information returned by :meth:`format`. - If *show_lines* is false, source code lines from being included in the output. + If *show_lines* is true, source code lines are included in the output. If *recent_first* is true, the exception is printed first followed by stack trace by "most recent call first" order. @@ -501,7 +501,7 @@ the module-level functions described above. some containing internal newlines. :func:`~traceback.print_exception` is a wrapper around this method which just prints the lines to a file. - If *show_lines* is false, source code lines from being included in the output. + If *show_lines* is true, source code lines are included in the output. If *recent_first* is true, the exception is printed first followed by stack trace by "most recent call first" order. @@ -585,8 +585,7 @@ the module-level functions described above. repetitions are shown, followed by a summary line stating the exact number of further repetitions. - The keyword argument *show_lines*, if ``False``, prevents source code - lines from being included in the output. + If *show_lines* is true, includes source code lines in the output. .. versionchanged:: 3.6 Long sequences of repeated frames are now abbreviated. @@ -602,8 +601,8 @@ the module-level functions described above. printed by :meth:`StackSummary.format`. If it returns ``None``, the frame is omitted from the output. - The keyword argument *show_lines*, if ``False``, prevents source code - lines from being included in the output. + The keyword argument *show_lines*, if ``True``, includes source code + lines in the output. .. versionadded:: 3.11 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9298551550312f..500fea10e574bc 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -174,13 +174,13 @@ tarfile traceback ---------- -* Add new ``show_lines`` and ``recent_first`` keyword only arguments to +* Add new *show_lines* and *recent_first* keyword only arguments to the :mod:`traceback` functions. - The ``show_lines`` argument controls whether source code lines are displayed. + The *show_lines* argument controls whether source code lines are displayed. It is default to ``True``. - The ``recent_first`` argument controls whether the most recent frames are + The *recent_first* argument controls whether the most recent frames are displayed first or last in the traceback. It affects whether the exception is displayed at the top or bottom of the traceback. It is default to ``False``. (Contributed by Inada Naoki in :gh:`135751`) From 8d38237ef854d50bc7c4b1105e2bb3a638111efa Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sat, 21 Jun 2025 17:09:20 +0900 Subject: [PATCH 13/13] fix doc --- Doc/library/traceback.rst | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 379ed42b4b51ae..9493b1924d7f67 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -67,14 +67,11 @@ Module-Level Functions If *recent_first* is true, the most recent stack trace entries are printed first, otherwise the oldest entries are printed first. The default is false. - - .. note:: - ``recent_first=True`` is useful for showing stack traces in places where - people see the top of the stack trace first, such as in a web browser. - - ``recent_first=False`` is useful for showing stack traces in places where - people see the bottom of the stack trace first, such as a console or log - files watched with :command:`tail -f`. + ``recent_first=True`` is useful for showing stack traces in places where + people see the top of the stack trace first, such as in a web browser. + ``recent_first=False`` is useful for showing stack traces in places where + people see the bottom of the stack trace first, such as a console or log + files watched with :command:`tail -f`. .. note:: @@ -88,7 +85,7 @@ Module-Level Functions Added negative *limit* support. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: print_exception(exc, /[, value, tb], limit=None, \ @@ -134,7 +131,7 @@ Module-Level Functions positional-only. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: print_exc(limit=None, file=None, chain=True, *, show_lines=True, recent_first=False) @@ -143,7 +140,7 @@ Module-Level Functions chain=chain, show_lines=show_lines, recent_first=recent_first)``. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: print_last(limit=None, file=None, chain=True, *, show_lines=True, recent_first=False) @@ -154,7 +151,7 @@ Module-Level Functions prompt (see :data:`sys.last_exc`). .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: print_stack(f=None, limit=None, file=None, *, show_lines=True, recent_first=False) @@ -172,7 +169,7 @@ Module-Level Functions Added negative *limit* support. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: extract_tb(tb, limit=None) @@ -204,7 +201,7 @@ Module-Level Functions If *show_lines* is true, source code lines are included in the output. .. versionchanged:: next - Added *show_lines* parameter. + Added the *show_lines* parameter. .. function:: format_list(extracted_list, *, show_lines=True) @@ -218,7 +215,7 @@ Module-Level Functions are included in the output. .. versionchanged:: next - Added *show_lines* parameter. + Added the *show_lines* parameter. .. function:: format_exception_only(exc, /[, value], *, show_group=False) @@ -271,7 +268,7 @@ Module-Level Functions :func:`print_exception`. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: format_exc(limit=None, chain=True, *, show_lines=True, recent_first=False) @@ -284,7 +281,7 @@ Module-Level Functions first, otherwise the oldest entries are printed first. The default is false. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: format_tb(tb, limit=None, *, show_lines=True, recent_first=False) @@ -294,7 +291,7 @@ Module-Level Functions If *recent_first* is true, ``reversed(extract_tb(tb, limit))`` is used. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: format_stack(f=None, limit=None, *, show_lines=True, recent_first=False) @@ -304,7 +301,7 @@ Module-Level Functions If *recent_first* is true, ``reversed(extract_stack(f, limit))`` is used. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. function:: clear_frames(tb) @@ -488,7 +485,7 @@ the module-level functions described above. .. versionadded:: 3.11 .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. method:: format(*, chain=True, show_lines=True, recent_first=False) @@ -509,7 +506,7 @@ the module-level functions described above. followed by the exception. .. versionchanged:: next - Added *show_lines* and *recent_first* parameters. + Added the *show_lines* and *recent_first* parameters. .. method:: format_exception_only(*, show_group=False)