diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index df4a38c955511b..869eb89138a282 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -212,10 +212,10 @@ The module also defines the following classes: :class:`TracebackException` objects are created from actual exceptions to capture data for later printing in a lightweight fashion. -.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False) +.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, format_locals=None, compact=False) - Capture an exception for later rendering. *limit*, *lookup_lines* and - *capture_locals* are as for the :class:`StackSummary` class. + Capture an exception for later rendering. *limit*, *lookup_lines*, + *capture_locals* and *format_locals* are as for the :class:`StackSummary` class. If *compact* is true, only data that is required by :class:`TracebackException`'s ``format`` method is saved in the class attributes. In particular, the @@ -264,10 +264,10 @@ capture data for later printing in a lightweight fashion. 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=True, capture_locals=False, format_locals=None) - Capture an exception for later rendering. *limit*, *lookup_lines* and - *capture_locals* are as for the :class:`StackSummary` class. + Capture an exception for later rendering. *limit*, *lookup_lines*, + *capture_locals* and *format_locals* are as for the :class:`StackSummary` class. Note that when locals are captured, they are also shown in the traceback. @@ -319,7 +319,7 @@ capture data for later printing in a lightweight fashion. .. class:: StackSummary - .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False) + .. classmethod:: extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False, format_locals=None) Construct a :class:`StackSummary` object from a frame generator (such as is returned by :func:`~traceback.walk_stack` or @@ -331,7 +331,13 @@ capture data for later printing in a lightweight fashion. creating the :class:`StackSummary` cheaper (which may be valuable if it may not actually get formatted). If *capture_locals* is ``True`` the local variables in each :class:`FrameSummary` are captured as object - representations. + representations. If *format_locals* is provided, it is used to + generate a :class:`dict` of string representations for a frame's + local variables. + + .. versionchanged:: XXX + Added the *format_locals* parameter. + .. classmethod:: from_list(a_list) @@ -520,6 +526,54 @@ The following example shows the different ways to print and format the stack:: ' File "", line 3, in another_function\n lumberstack()\n', ' File "", line 8, in lumberstack\n print(repr(traceback.format_stack()))\n'] +The following example shows how to use *format_locals* to filter and change the +formatting of local variables. + +.. testcode:: format_locals + + import traceback + from unittest.util import safe_repr + + def format_locals(locals): + return { + k: safe_repr(v) # Handle exceptions thrown by __repr__ + for k, v in locals.items() + if not k.startswith("_") # Hide private variables + } + + class A: + def __repr__(self): + raise ValueError("Unrepresentable") + + try: + a = A() + _pw = "supersecretpassword" + 1 / 0 + except Exception as e: + print( + "".join( + traceback.TracebackException.from_exception( + e, limit=1, capture_locals=True, format_locals=format_locals + ).format() + ) + ) + +The output would look like this: + +.. testoutput:: format_locals + :options: +NORMALIZE_WHITESPACE + + Traceback (most recent call last): + File "...", line 18, in + 1 / 0 + ~~^~~ + A = + a = + e = ZeroDivisionError('division by zero') + format_locals = + safe_repr = + traceback = + ZeroDivisionError: division by zero This last example demonstrates the final few formatting functions: diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index a6eee22fa332cd..e17d32cdcea5e7 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1380,6 +1380,8 @@ Basic customization This is typically used for debugging, so it is important that the representation is information-rich and unambiguous. + Furthermore, this function should avoid to raise exceptions because that can + lead to problems in some debugging contexts. .. index:: single: string; __str__() (object method) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index d88851ddda4313..30c3f3a1760aec 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -12,6 +12,7 @@ requires_debug_ranges, has_no_debug_ranges) from test.support.os_helper import TESTFN, unlink from test.support.script_helper import assert_python_ok, assert_python_failure +from unittest.util import safe_repr import os import textwrap @@ -1868,7 +1869,7 @@ def test_no_locals(self): s = traceback.StackSummary.extract(iter([(f, 6)])) self.assertEqual(s[0].locals, None) - def test_format_locals(self): + def test_format_locals_default(self): def some_inner(k, v): a = 1 b = 2 @@ -1884,6 +1885,26 @@ def some_inner(k, v): ' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3) ], s.format()) + def test_format_locals_callback(self): + def _format_locals(locals): + return {k: "<"+repr(v)+">" for k,v in locals.items() if not k.startswith("_")} + + def some_inner(k, v): + a = 1 + b = 2 + return traceback.StackSummary.extract( + traceback.walk_stack(None), capture_locals=True, format_locals=_format_locals, limit=1) + + s = some_inner(3, 4) + self.assertEqual( + [' File "%s", line %d, in some_inner\n' + ' return traceback.StackSummary.extract(\n' + ' a = <1>\n' + ' b = <2>\n' + ' k = <3>\n' + ' v = <4>\n' % (__file__, some_inner.__code__.co_firstlineno + 3) + ], s.format()) + def test_custom_format_frame(self): class CustomStackSummary(traceback.StackSummary): def format_frame_summary(self, frame_summary): @@ -1899,32 +1920,32 @@ def some_inner(): [f'{__file__}:{some_inner.__code__.co_firstlineno + 1}']) def test_dropping_frames(self): - def f(): - 1/0 + def f(): + 1/0 - def g(): - try: - f() - except: - return sys.exc_info() + def g(): + try: + f() + except: + return sys.exc_info() - exc_info = g() + exc_info = g() - class Skip_G(traceback.StackSummary): - def format_frame_summary(self, frame_summary): - if frame_summary.name == 'g': - return None - return super().format_frame_summary(frame_summary) + class Skip_G(traceback.StackSummary): + def format_frame_summary(self, frame_summary): + if frame_summary.name == 'g': + return None + return super().format_frame_summary(frame_summary) - stack = Skip_G.extract( - traceback.walk_tb(exc_info[2])).format() + stack = Skip_G.extract( + traceback.walk_tb(exc_info[2])).format() - self.assertEqual(len(stack), 1) - lno = f.__code__.co_firstlineno + 1 - self.assertEqual( - stack[0], - f' File "{__file__}", line {lno}, in f\n 1/0\n' - ) + self.assertEqual(len(stack), 1) + lno = f.__code__.co_firstlineno + 1 + self.assertEqual( + stack[0], + f' File "{__file__}", line {lno}, in f\n 1/0\n' + ) class TestTracebackException(unittest.TestCase): @@ -1966,6 +1987,36 @@ def foo(): self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) + def test_from_exception_format_locals(self): + # Check that format_locals works as expected. + + def format_locals(locals): + return {k: safe_repr(v) for k,v in locals.items()} + + class FailingInit: + def __init__(self) -> None: + self.x = 1/0 + def __repr__(self): + return self.x + + try: + FailingInit() + except Exception as e: + exc_info = sys.exc_info() + self.expected_stack = traceback.StackSummary.extract( + traceback.walk_tb(exc_info[2]), limit=2, lookup_lines=False, + capture_locals=True, format_locals=format_locals) + self.exc = traceback.TracebackException.from_exception( + e, limit=2, lookup_lines=False, capture_locals=True, format_locals=format_locals) + expected_stack = self.expected_stack + exc = self.exc + self.assertEqual(None, exc.__cause__) + self.assertEqual(None, exc.__context__) + self.assertEqual(False, exc.__suppress_context__) + self.assertEqual(expected_stack, exc.stack) + self.assertEqual(exc_info[0], exc.exc_type) + self.assertEqual(str(exc_info[1]), str(exc)) + def test_cause(self): try: try: diff --git a/Lib/traceback.py b/Lib/traceback.py index 97caa1372f4788..dec09a5c565301 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -251,7 +251,7 @@ class FrameSummary: - :attr:`line` The text from the linecache module for the of code that was running when the frame was captured. - :attr:`locals` Either None if locals were not supplied, or a dict - mapping the name to the repr() of the variable. + mapping the name to a string representation of the variable. """ __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno', @@ -259,15 +259,24 @@ class FrameSummary: def __init__(self, filename, lineno, name, *, lookup_line=True, locals=None, line=None, - end_lineno=None, colno=None, end_colno=None): + end_lineno=None, colno=None, end_colno=None, format_locals=None): """Construct a FrameSummary. + :param filename: The filename for the frame. + :param lineno: The line within filename for the frame that was + active when the frame was captured. + :param name: The name of the function or method that was executing + when the frame was captured. :param lookup_line: If True, `linecache` is consulted for the source code line. Otherwise, the line will be looked up when first needed. :param locals: If supplied the frame locals, which will be captured as object representations. :param line: If provided, use this instead of looking up the line in the linecache. + :param end_lineno: The end linenumber where the error occurred. + :param format_locals: If provided, use this callable to transform + the supplied locals into a dictionary of string representations. + By default, repr() is applied to every value. """ self.filename = filename self.lineno = lineno @@ -275,7 +284,12 @@ def __init__(self, filename, lineno, name, *, lookup_line=True, self._line = line if lookup_line: self.line - self.locals = {k: repr(v) for k, v in locals.items()} if locals else None + + if locals: + self.locals = format_locals(locals) if format_locals is not None else {k: repr(v) for k, v in locals.items()} + else: + self.locals = None + self.end_lineno = end_lineno self.colno = colno self.end_colno = end_colno @@ -370,7 +384,7 @@ class StackSummary(list): @classmethod def extract(klass, frame_gen, *, limit=None, lookup_lines=True, - capture_locals=False): + capture_locals=False, format_locals=None): """Create a StackSummary from a traceback or stack object. :param frame_gen: A generator that yields (frame, lineno) tuples @@ -381,6 +395,9 @@ def extract(klass, frame_gen, *, limit=None, lookup_lines=True, otherwise lookup is deferred until the frame is rendered. :param capture_locals: If True, the local variables from each frame will be captured as object representations into the FrameSummary. + :param format_locals: If provided, this callable will be used to + transform the local variables in each frame into a dictionary + of string representations. """ def extended_frame_gen(): for f, lineno in frame_gen: @@ -388,11 +405,11 @@ def extended_frame_gen(): return klass._extract_from_extended_frame_gen( extended_frame_gen(), limit=limit, lookup_lines=lookup_lines, - capture_locals=capture_locals) + capture_locals=capture_locals, format_locals=format_locals) @classmethod def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None, - lookup_lines=True, capture_locals=False): + lookup_lines=True, capture_locals=False, format_locals=None): # 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 @@ -423,7 +440,7 @@ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None, f_locals = None result.append(FrameSummary( filename, lineno, name, lookup_line=False, locals=f_locals, - end_lineno=end_lineno, colno=colno, end_colno=end_colno)) + end_lineno=end_lineno, colno=colno, end_colno=end_colno, format_locals=format_locals)) for filename in fnames: linecache.checkcache(filename) # If immediate lookup was desired, trigger lookups now. @@ -663,7 +680,7 @@ class TracebackException: """ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, - lookup_lines=True, capture_locals=False, compact=False, + lookup_lines=True, capture_locals=False, format_locals=None, compact=False, max_group_width=15, max_group_depth=10, _seen=None): # NB: we need to accept exc_traceback, exc_value, exc_traceback to # permit backwards compat with the existing API, otherwise we @@ -680,7 +697,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, self.stack = StackSummary._extract_from_extended_frame_gen( _walk_tb_with_full_positions(exc_traceback), limit=limit, lookup_lines=lookup_lines, - capture_locals=capture_locals) + capture_locals=capture_locals, format_locals=format_locals) self.exc_type = exc_type # Capture now to permit freeing resources: only complication is in the # unofficial API _format_final_exc_line @@ -716,6 +733,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + format_locals=format_locals, max_group_width=max_group_width, max_group_depth=max_group_depth, _seen=_seen) @@ -737,6 +755,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, limit=limit, lookup_lines=lookup_lines, capture_locals=capture_locals, + format_locals=format_locals, max_group_width=max_group_width, max_group_depth=max_group_depth, _seen=_seen) diff --git a/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst b/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst new file mode 100644 index 00000000000000..78d7b87d0e6a44 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-10-28-21-02-01.bpo-43656.Gd7f7D.rst @@ -0,0 +1 @@ +Introduce format_locals in traceback \ No newline at end of file