diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index b5464ac55ddfa9..9493b1924d7f67 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,16 @@ Module-Level Functions :term:`file ` or :term:`file-like object` to receive 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. + ``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 +84,12 @@ Module-Level Functions .. versionchanged:: 3.5 Added negative *limit* support. + .. versionchanged:: next + Added the *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 ` @@ -105,6 +118,11 @@ Module-Level Functions printed as well, like the interpreter itself does when printing an unhandled exception. + 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. + .. versionchanged:: 3.5 The *etype* argument is ignored and inferred from the type of *value*. @@ -112,21 +130,31 @@ Module-Level Functions The *etype* parameter has been renamed to *exc* and is now positional-only. + .. versionchanged:: next + Added the *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 the *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 the *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 +162,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 true, source code lines are + included in the output. .. versionchanged:: 3.5 - Added negative *limit* support. + Added negative *limit* support. + + .. versionchanged:: next + Added the *show_lines* and *recent_first* parameters. .. function:: extract_tb(tb, limit=None) @@ -161,21 +193,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 true, source code lines are included in the output. + + .. versionchanged:: next + Added the *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 ``True``, source code lines + are included in the output. + + .. versionchanged:: next + Added the *show_lines* parameter. .. function:: format_exception_only(exc, /[, value], *, show_group=False) @@ -208,7 +248,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 +256,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 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. + .. versionchanged:: 3.5 The *etype* argument is ignored and inferred from the type of *value*. @@ -223,21 +267,41 @@ Module-Level Functions This function's behavior and signature were modified to match :func:`print_exception`. + .. versionchanged:: next + Added the *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 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. + + .. versionchanged:: next + Added the *show_lines* and *recent_first* parameters. + + +.. function:: format_tb(tb, limit=None, *, show_lines=True, recent_first=False) -.. function:: format_tb(tb, limit=None) + A shorthand for ``format_list(extract_tb(tb, limit), show_lines=show_lines)``. - A shorthand for ``format_list(extract_tb(tb, limit))``. + If *recent_first* is true, ``reversed(extract_tb(tb, limit))`` is used. + .. versionchanged:: next + Added the *show_lines* and *recent_first* parameters. -.. function:: format_stack(f=None, limit=None) - A shorthand for ``format_list(extract_stack(f, limit))``. +.. 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 the *show_lines* and *recent_first* parameters. .. function:: clear_frames(tb) @@ -283,7 +347,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` @@ -309,6 +373,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 @@ -391,21 +459,35 @@ 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. - .. method:: print(*, file=None, chain=True) + .. 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 :meth:`format`. + 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. + 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 the *show_lines* and *recent_first* parameters. + + .. method:: format(*, chain=True, show_lines=True, recent_first=False) Format the exception. @@ -416,6 +498,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 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. + Otherwise, the stack trace is printed first by "most recent call last" order + followed by the exception. + + .. versionchanged:: next + Added the *show_lines* and *recent_first* parameters. + .. method:: format_exception_only(*, show_group=False) Format the exception part of the traceback. @@ -449,7 +541,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 @@ -467,6 +559,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 @@ -474,7 +570,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 +582,15 @@ the module-level functions described above. repetitions are shown, followed by a summary line stating the exact number of further repetitions. + If *show_lines* is true, includes source code lines 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 +598,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 ``True``, includes source code + lines in the output. + .. versionadded:: 3.11 + .. versionchanged:: next + Added the *show_lines* parameter. + :class:`!FrameSummary` Objects ------------------------------ diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..500fea10e574bc 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -171,6 +171,21 @@ tarfile and :cve:`2025-4435`.) +traceback +---------- + +* 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 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`) + + zlib ---- diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 74b979d009664d..5117c7ed959f4f 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, **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, **kwargs)')) + 'chain=True, *, show_lines=True, recent_first=False, **kwargs)')) self.assertEqual( str(inspect.signature(traceback.format_exception_only)), @@ -3340,7 +3341,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 +3366,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 +4878,245 @@ 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) + + +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() diff --git a/Lib/traceback.py b/Lib/traceback.py index a1f175dbbaa421..f2f188d60d51b9 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -26,45 +26,63 @@ # -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. + + To print in "recent call first" order, call "extracted_list.reverse()" + before passing it to this function. + + If 'show_lines' is true, source code lines are 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 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 true, source code lines are 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, 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' 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 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. """ - print_list(extract_tb(tb, limit=limit), file=file) + 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): +def format_tb(tb, limit=None, *, show_lines=True, recent_first=False): """A shorthand for 'format_list(extract_tb(tb, limit))'.""" - return extract_tb(tb, limit=limit).format() + + tblist = extract_tb(tb, limit=limit) + if recent_first: + tblist.reverse() + return tblist.format(show_lines=show_lines) def extract_tb(tb, limit=None): """ @@ -116,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, **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 @@ -127,11 +146,16 @@ 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 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. """ 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.print(file=file, chain=chain, colorize=colorize, + show_lines=show_lines, recent_first=recent_first) BUILTIN_EXCEPTION_LIMIT = object() @@ -143,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, **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 @@ -156,7 +180,8 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \ 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)) + 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): @@ -205,47 +230,63 @@ 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, + 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): +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)) + 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): - """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, *, 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) + 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) + 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): +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 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: f = sys._getframe().f_back - print_list(extract_stack(f, limit=limit), file=file) + 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): - """Shorthand for 'format_list(extract_stack(f, limit))'.""" +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)) + 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): @@ -260,6 +301,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) + # Traceback should use "recent call last" order. stack.reverse() return stack @@ -435,8 +477,8 @@ class StackSummary(list): """A list of FrameSummary objects, representing a stack of frames.""" @classmethod - def extract(klass, frame_gen, *, limit=None, lookup_lines=True, - capture_locals=False): + def extract(klass, frame_gen, *, limit=None, lookup_lines=False, + capture_locals=False): """Create a StackSummary from a traceback or stack object. :param frame_gen: A generator that yields (frame, lineno) tuples @@ -458,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 @@ -525,7 +567,7 @@ 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 @@ -553,7 +595,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 +784,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 @@ -761,7 +803,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 @@ -1042,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 @@ -1465,7 +1507,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, recent_first=False, **kwargs): """Format the exception. If chain is not *True*, *__cause__* and *__context__* will not be formatted. @@ -1505,10 +1547,16 @@ def format(self, *, chain=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(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( @@ -1519,13 +1567,22 @@ def format(self, *, chain=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(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 @@ -1548,7 +1605,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 +1623,13 @@ def format(self, *, chain=True, _ctx=None, **kwargs): _ctx.exception_group_depth = 0 - def print(self, *, file=None, chain=True, **kwargs): + 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, colorize=colorize): + for line in self.format(chain=chain, show_lines=show_lines, + recent_first=recent_first, colorize=colorize): print(line, file=file, end="") 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.