Issue #22936: Make it possible to show local variables in tracebacks.
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 3c32273..d9b73c1 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -15,7 +15,7 @@
test_code = namedtuple('code', ['co_filename', 'co_name'])
-test_frame = namedtuple('frame', ['f_code', 'f_globals'])
+test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals'])
test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next'])
@@ -535,7 +535,7 @@
linecache.clearcache()
linecache.updatecache('/foo.py', globals())
c = test_code('/foo.py', 'method')
- f = test_frame(c, None)
+ f = test_frame(c, None, None)
s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=True)
linecache.clearcache()
self.assertEqual(s[0].line, "import sys")
@@ -543,14 +543,14 @@
def test_extract_stackup_deferred_lookup_lines(self):
linecache.clearcache()
c = test_code('/foo.py', 'method')
- f = test_frame(c, None)
+ f = test_frame(c, None, None)
s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=False)
self.assertEqual({}, linecache.cache)
linecache.updatecache('/foo.py', globals())
self.assertEqual(s[0].line, "import sys")
def test_from_list(self):
- s = traceback.StackSummary([('foo.py', 1, 'fred', 'line')])
+ s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')])
self.assertEqual(
[' File "foo.py", line 1, in fred\n line\n'],
s.format())
@@ -558,11 +558,42 @@
def test_format_smoke(self):
# For detailed tests see the format_list tests, which consume the same
# code.
- s = traceback.StackSummary([('foo.py', 1, 'fred', 'line')])
+ s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')])
self.assertEqual(
[' File "foo.py", line 1, in fred\n line\n'],
s.format())
+ def test_locals(self):
+ linecache.updatecache('/foo.py', globals())
+ c = test_code('/foo.py', 'method')
+ f = test_frame(c, globals(), {'something': 1})
+ s = traceback.StackSummary.extract(iter([(f, 6)]), capture_locals=True)
+ self.assertEqual(s[0].locals, {'something': '1'})
+
+ def test_no_locals(self):
+ linecache.updatecache('/foo.py', globals())
+ c = test_code('/foo.py', 'method')
+ f = test_frame(c, globals(), {'something': 1})
+ s = traceback.StackSummary.extract(iter([(f, 6)]))
+ self.assertEqual(s[0].locals, None)
+
+ def test_format_locals(self):
+ def some_inner(k, v):
+ a = 1
+ b = 2
+ return traceback.StackSummary.extract(
+ traceback.walk_stack(None), capture_locals=True, limit=1)
+ s = some_inner(3, 4)
+ self.assertEqual(
+ [' File "' + __file__ + '", line 585, '
+ 'in some_inner\n'
+ ' traceback.walk_stack(None), capture_locals=True, limit=1)\n'
+ ' a = 1\n'
+ ' b = 2\n'
+ ' k = 3\n'
+ ' v = 4\n'
+ ], s.format())
+
class TestTracebackException(unittest.TestCase):
@@ -591,9 +622,10 @@
except Exception as e:
exc_info = sys.exc_info()
self.expected_stack = traceback.StackSummary.extract(
- traceback.walk_tb(exc_info[2]), limit=1, lookup_lines=False)
+ traceback.walk_tb(exc_info[2]), limit=1, lookup_lines=False,
+ capture_locals=True)
self.exc = traceback.TracebackException.from_exception(
- e, limit=1, lookup_lines=False)
+ e, limit=1, lookup_lines=False, capture_locals=True)
expected_stack = self.expected_stack
exc = self.exc
self.assertEqual(None, exc.__cause__)
@@ -664,13 +696,33 @@
linecache.clearcache()
e = Exception("uh oh")
c = test_code('/foo.py', 'method')
- f = test_frame(c, None)
+ f = test_frame(c, None, None)
tb = test_tb(f, 6, None)
exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False)
self.assertEqual({}, linecache.cache)
linecache.updatecache('/foo.py', globals())
self.assertEqual(exc.stack[0].line, "import sys")
+ def test_locals(self):
+ linecache.updatecache('/foo.py', globals())
+ e = Exception("uh oh")
+ c = test_code('/foo.py', 'method')
+ f = test_frame(c, globals(), {'something': 1, 'other': 'string'})
+ tb = test_tb(f, 6, None)
+ exc = traceback.TracebackException(
+ Exception, e, tb, capture_locals=True)
+ self.assertEqual(
+ exc.stack[0].locals, {'something': '1', 'other': "'string'"})
+
+ def test_no_locals(self):
+ linecache.updatecache('/foo.py', globals())
+ e = Exception("uh oh")
+ c = test_code('/foo.py', 'method')
+ f = test_frame(c, globals(), {'something': 1})
+ tb = test_tb(f, 6, None)
+ exc = traceback.TracebackException(Exception, e, tb)
+ self.assertEqual(exc.stack[0].locals, None)
+
def test_main():
run_unittest(__name__)
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 72e1e2a..0ac1819 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -223,19 +223,19 @@
- :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 str() of the variable.
+ mapping the name to the repr() of the variable.
"""
__slots__ = ('filename', 'lineno', 'name', '_line', 'locals')
- def __init__(self, filename, lineno, name, lookup_line=True, locals=None,
- line=None):
+ def __init__(self, filename, lineno, name, *, lookup_line=True,
+ locals=None, line=None):
"""Construct a FrameSummary.
: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
- strings.
+ object representations.
:param line: If provided, use this instead of looking up the line in
the linecache.
"""
@@ -246,7 +246,7 @@
if lookup_line:
self.line
self.locals = \
- dict((k, str(v)) for k, v in locals.items()) if locals else None
+ dict((k, repr(v)) for k, v in locals.items()) if locals else None
def __eq__(self, other):
return (self.filename == other.filename and
@@ -299,7 +299,8 @@
"""A stack of frames."""
@classmethod
- def extract(klass, frame_gen, limit=None, lookup_lines=True):
+ def extract(klass, frame_gen, *, limit=None, lookup_lines=True,
+ capture_locals=False):
"""Create a StackSummary from a traceback or stack object.
:param frame_gen: A generator that yields (frame, lineno) tuples to
@@ -308,6 +309,8 @@
include.
:param lookup_lines: If True, lookup lines for each frame immediately,
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.
"""
if limit is None:
limit = getattr(sys, 'tracebacklimit', None)
@@ -324,7 +327,12 @@
fnames.add(filename)
linecache.lazycache(filename, f.f_globals)
# Must defer line lookups until we have called checkcache.
- result.append(FrameSummary(filename, lineno, name, lookup_line=False))
+ if capture_locals:
+ f_locals = f.f_locals
+ else:
+ f_locals = None
+ result.append(FrameSummary(
+ filename, lineno, name, lookup_line=False, locals=f_locals))
for filename in fnames:
linecache.checkcache(filename)
# If immediate lookup was desired, trigger lookups now.
@@ -356,11 +364,16 @@
newlines as well, for those items with source text lines.
"""
result = []
- for filename, lineno, name, line in self:
- item = ' File "{}", line {}, in {}\n'.format(filename, lineno, name)
- if line:
- item = item + ' {}\n'.format(line.strip())
- result.append(item)
+ for frame in self:
+ row = []
+ row.append(' File "{}", line {}, in {}\n'.format(
+ frame.filename, frame.lineno, frame.name))
+ if frame.line:
+ row.append(' {}\n'.format(frame.line.strip()))
+ if frame.locals:
+ for name, value in sorted(frame.locals.items()):
+ row.append(' {name} = {value}\n'.format(name=name, value=value))
+ result.append(''.join(row))
return result
@@ -392,8 +405,8 @@
- :attr:`msg` For syntax errors - the compiler error message.
"""
- def __init__(self, exc_type, exc_value, exc_traceback, limit=None,
- lookup_lines=True, _seen=None):
+ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
+ lookup_lines=True, capture_locals=False, _seen=None):
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
# permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together.
@@ -411,6 +424,7 @@
exc_value.__cause__.__traceback__,
limit=limit,
lookup_lines=False,
+ capture_locals=capture_locals,
_seen=_seen)
else:
cause = None
@@ -422,6 +436,7 @@
exc_value.__context__.__traceback__,
limit=limit,
lookup_lines=False,
+ capture_locals=capture_locals,
_seen=_seen)
else:
context = None
@@ -431,7 +446,8 @@
exc_value.__suppress_context__ if exc_value else False
# TODO: locals.
self.stack = StackSummary.extract(
- walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines)
+ walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
+ capture_locals=capture_locals)
self.exc_type = exc_type
# Capture now to permit freeing resources: only complication is in the
# unofficial API _format_final_exc_line
@@ -512,7 +528,7 @@
msg = self.msg or "<no detail available>"
yield "{}: {}\n".format(stype, msg)
- def format(self, chain=True):
+ def format(self, *, chain=True):
"""Format the exception.
If chain is not *True*, *__cause__* and *__context__* will not be formatted.