Issue #22936: Make it possible to show local variables in tracebacks.

This commit is contained in:
Robert Collins 2015-03-05 20:28:52 +13:00
parent 65e62e6fe7
commit a11cd2be1f
2 changed files with 91 additions and 22 deletions

View File

@ -226,19 +226,19 @@ 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 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.
"""
@ -249,7 +249,7 @@ class FrameSummary:
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
@ -302,7 +302,8 @@ class StackSummary(list):
"""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
@ -311,6 +312,8 @@ class StackSummary(list):
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)
@ -327,7 +330,12 @@ class StackSummary(list):
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.
@ -359,11 +367,16 @@ class StackSummary(list):
newlines as well, for those items with source text lines.
"""
result = []
for filename, lineno, name, line in self:
item = u(' File "{0}", line {1}, in {2}\n').format(filename, lineno, name)
if line:
item = item + u(' {0}\n').format(line.strip())
result.append(item)
for frame in self:
row = []
row.append(u(' File "{0}", line {1}, in {2}\n').format(
frame.filename, frame.lineno, frame.name))
if frame.line:
row.append(u(' {0}\n').format(frame.line.strip()))
if frame.locals:
for name, value in sorted(frame.locals.items()):
row.append(u(' {name} = {value}\n').format(name=name, value=value))
result.append(u('').join(row))
return result
@ -396,7 +409,7 @@ class TracebackException:
"""
def __init__(self, exc_type, exc_value, exc_traceback, limit=None,
lookup_lines=True, _seen=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.
@ -414,6 +427,7 @@ class TracebackException:
exc_value.__cause__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
else:
cause = None
@ -425,6 +439,7 @@ class TracebackException:
exc_value.__context__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
else:
context = None
@ -434,7 +449,8 @@ class TracebackException:
getattr(exc_value, '__suppress_context__', False) 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

View File

@ -53,7 +53,7 @@ fake_module = dict(
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'])
@ -569,7 +569,7 @@ class TestStack(unittest.TestCase):
linecache.clearcache()
linecache.updatecache('/foo.py', fake_module)
c = test_code('/foo.py', 'method')
f = test_frame(c, None)
f = test_frame(c, None, None)
s = traceback.StackSummary.extract(iter([(f, 8)]), lookup_lines=True)
linecache.clearcache()
self.assertEqual(s[0].line, "import sys")
@ -577,14 +577,14 @@ class TestStack(unittest.TestCase):
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, 8)]), lookup_lines=False)
self.assertEqual({}, linecache.cache)
linecache.updatecache('/foo.py', fake_module)
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())
@ -592,11 +592,42 @@ class TestStack(unittest.TestCase):
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 619, '
'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):
@ -626,9 +657,10 @@ class TestTracebackException(unittest.TestCase):
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__)
@ -702,9 +734,30 @@ class TestTracebackException(unittest.TestCase):
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, 8, None)
exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False)
self.assertEqual({}, linecache.cache)
linecache.updatecache('/foo.py', fake_module)
self.assertEqual(exc.stack[0].line, "import sys")
def test_locals(self):
linecache.updatecache('/foo.py', fake_module)
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', fake_module)
e = Exception("uh oh")
c = test_code('/foo.py', 'method')
f = test_frame(c, fake_module, {'something': 1})
tb = test_tb(f, 6, None)
exc = traceback.TracebackException(Exception, e, tb)
self.assertEqual(exc.stack[0].locals, None)