Thanks to visit codestin.com
Credit goes to github.com

Skip to content

bpo-43656: Introduce format_locals in traceback #29299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -520,6 +526,54 @@ The following example shows the different ways to print and format the stack::
' File "<doctest>", line 3, in another_function\n lumberstack()\n',
' File "<doctest>", 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 <module>
1 / 0
~~^~~
A = <class 'A'>
a = <A object at 0x...>
e = ZeroDivisionError('division by zero')
format_locals = <function format_locals at 0x...>
safe_repr = <function safe_repr at 0x...>
traceback = <module 'traceback' from ...>
ZeroDivisionError: division by zero

This last example demonstrates the final few formatting functions:

Expand Down
2 changes: 2 additions & 0 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
95 changes: 73 additions & 22 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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'
)
Comment on lines +1923 to +1948
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cosmetic change was done by make patchcheck



class TestTracebackException(unittest.TestCase):
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 28 additions & 9 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,31 +251,45 @@ 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',
'name', '_line', 'locals')

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
self.name = name
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
Expand Down Expand Up @@ -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
Expand All @@ -381,18 +395,21 @@ 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:
yield f, (lineno, None, None, None)

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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce format_locals in traceback