Issue #22936: Make it possible to show local variables in tracebacks.
This commit is contained in:
parent
2856332f5e
commit
d7c7e0ef69
|
@ -159,17 +159,21 @@ The module also defines the following classes:
|
||||||
:class:`.TracebackException` objects are created from actual exceptions to
|
:class:`.TracebackException` objects are created from actual exceptions to
|
||||||
capture data for later printing in a lightweight fashion.
|
capture data for later printing in a lightweight fashion.
|
||||||
|
|
||||||
.. class:: TracebackException(exc_type, exc_value, exc_traceback, limit=None, lookup_lines=True)
|
.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False)
|
||||||
|
|
||||||
Capture an exception for later rendering. limit, lookup_lines are as for
|
Capture an exception for later rendering. limit, lookup_lines and
|
||||||
the :class:`.StackSummary` class.
|
capture_locals=False are as for the :class:`.StackSummary` class.
|
||||||
|
|
||||||
|
Note that when locals are captured, they are also shown in the traceback.
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
|
|
||||||
.. classmethod:: `.from_exception`(exc, limit=None, lookup_lines=True)
|
.. classmethod:: `.from_exception`(exc, *, limit=None, lookup_lines=True, capture_locals=False)
|
||||||
|
|
||||||
Capture an exception for later rendering. limit and lookup_lines
|
Capture an exception for later rendering. limit, lookup_lines and
|
||||||
are as for the :class:`.StackSummary` class.
|
capture_locals=False are as for the :class:`.StackSummary` class.
|
||||||
|
|
||||||
|
Note that when locals are captured, they are also shown in the traceback.
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
|
|
||||||
|
@ -190,7 +194,7 @@ capture data for later printing in a lightweight fashion.
|
||||||
error occured.
|
error occured.
|
||||||
.. attribute:: `.msg` For syntax errors - the compiler error message.
|
.. attribute:: `.msg` For syntax errors - the compiler error message.
|
||||||
|
|
||||||
.. method:: TracebackException.format(chain=True)
|
.. method:: TracebackException.format(*, chain=True)
|
||||||
|
|
||||||
Format the exception.
|
Format the exception.
|
||||||
|
|
||||||
|
@ -227,7 +231,7 @@ capture data for later printing in a lightweight fashion.
|
||||||
|
|
||||||
:class:`.StackSummary` objects represent a call stack ready for formatting.
|
:class:`.StackSummary` objects represent a call stack ready for formatting.
|
||||||
|
|
||||||
.. classmethod:: StackSummary.extract(frame_gen, limit=None, lookup_lines=True)
|
.. classmethod:: StackSummary.extract(frame_gen, *, limit=None, lookup_lines=True, capture_locals=False)
|
||||||
|
|
||||||
Construct a StackSummary object from a frame generator (such as is returned by
|
Construct a StackSummary object from a frame generator (such as is returned by
|
||||||
`walk_stack` or `walk_tb`.
|
`walk_stack` or `walk_tb`.
|
||||||
|
@ -236,6 +240,8 @@ capture data for later printing in a lightweight fashion.
|
||||||
If lookup_lines is False, the returned FrameSummary objects will not have read
|
If lookup_lines is False, the returned FrameSummary objects will not have read
|
||||||
their lines in yet, making the cost of creating the StackSummary cheaper (which
|
their lines in yet, making the cost of creating the StackSummary cheaper (which
|
||||||
may be valuable if it may not actually get formatted).
|
may be valuable if it may not actually get formatted).
|
||||||
|
If capture_locals is True the local variables in each *FrameSummary* are
|
||||||
|
captured as object representations.
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
|
|
||||||
|
@ -258,8 +264,10 @@ FrameSummary objects represent a single frame in a traceback.
|
||||||
or printed. It may optionally have a stringified version of the frames
|
or printed. It may optionally have a stringified version of the frames
|
||||||
locals included in it. If *lookup_line* is False, the source code is not
|
locals included in it. If *lookup_line* is False, the source code is not
|
||||||
looked up until the FrameSummary has the :attr:`line` attribute accessed (which
|
looked up until the FrameSummary has the :attr:`line` attribute accessed (which
|
||||||
also happens when casting it to a tuple). Line may be directly provided, and
|
also happens when casting it to a tuple). *line* may be directly provided, and
|
||||||
will prevent line lookups happening at all.
|
will prevent line lookups happening at all. *locals* is an optional local variable
|
||||||
|
dictionary, and if supplied the variable representations are stored in the summary
|
||||||
|
for later display.
|
||||||
|
|
||||||
.. _traceback-example:
|
.. _traceback-example:
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import traceback
|
||||||
|
|
||||||
|
|
||||||
test_code = namedtuple('code', ['co_filename', 'co_name'])
|
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'])
|
test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -535,7 +535,7 @@ class TestStack(unittest.TestCase):
|
||||||
linecache.clearcache()
|
linecache.clearcache()
|
||||||
linecache.updatecache('/foo.py', globals())
|
linecache.updatecache('/foo.py', globals())
|
||||||
c = test_code('/foo.py', 'method')
|
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)
|
s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=True)
|
||||||
linecache.clearcache()
|
linecache.clearcache()
|
||||||
self.assertEqual(s[0].line, "import sys")
|
self.assertEqual(s[0].line, "import sys")
|
||||||
|
@ -543,14 +543,14 @@ class TestStack(unittest.TestCase):
|
||||||
def test_extract_stackup_deferred_lookup_lines(self):
|
def test_extract_stackup_deferred_lookup_lines(self):
|
||||||
linecache.clearcache()
|
linecache.clearcache()
|
||||||
c = test_code('/foo.py', 'method')
|
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)
|
s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=False)
|
||||||
self.assertEqual({}, linecache.cache)
|
self.assertEqual({}, linecache.cache)
|
||||||
linecache.updatecache('/foo.py', globals())
|
linecache.updatecache('/foo.py', globals())
|
||||||
self.assertEqual(s[0].line, "import sys")
|
self.assertEqual(s[0].line, "import sys")
|
||||||
|
|
||||||
def test_from_list(self):
|
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(
|
self.assertEqual(
|
||||||
[' File "foo.py", line 1, in fred\n line\n'],
|
[' File "foo.py", line 1, in fred\n line\n'],
|
||||||
s.format())
|
s.format())
|
||||||
|
@ -558,11 +558,42 @@ class TestStack(unittest.TestCase):
|
||||||
def test_format_smoke(self):
|
def test_format_smoke(self):
|
||||||
# For detailed tests see the format_list tests, which consume the same
|
# For detailed tests see the format_list tests, which consume the same
|
||||||
# code.
|
# code.
|
||||||
s = traceback.StackSummary([('foo.py', 1, 'fred', 'line')])
|
s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[' File "foo.py", line 1, in fred\n line\n'],
|
[' File "foo.py", line 1, in fred\n line\n'],
|
||||||
s.format())
|
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):
|
class TestTracebackException(unittest.TestCase):
|
||||||
|
@ -591,9 +622,10 @@ class TestTracebackException(unittest.TestCase):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exc_info = sys.exc_info()
|
exc_info = sys.exc_info()
|
||||||
self.expected_stack = traceback.StackSummary.extract(
|
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(
|
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
|
expected_stack = self.expected_stack
|
||||||
exc = self.exc
|
exc = self.exc
|
||||||
self.assertEqual(None, exc.__cause__)
|
self.assertEqual(None, exc.__cause__)
|
||||||
|
@ -664,13 +696,33 @@ class TestTracebackException(unittest.TestCase):
|
||||||
linecache.clearcache()
|
linecache.clearcache()
|
||||||
e = Exception("uh oh")
|
e = Exception("uh oh")
|
||||||
c = test_code('/foo.py', 'method')
|
c = test_code('/foo.py', 'method')
|
||||||
f = test_frame(c, None)
|
f = test_frame(c, None, None)
|
||||||
tb = test_tb(f, 6, None)
|
tb = test_tb(f, 6, None)
|
||||||
exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False)
|
exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False)
|
||||||
self.assertEqual({}, linecache.cache)
|
self.assertEqual({}, linecache.cache)
|
||||||
linecache.updatecache('/foo.py', globals())
|
linecache.updatecache('/foo.py', globals())
|
||||||
self.assertEqual(exc.stack[0].line, "import sys")
|
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():
|
def test_main():
|
||||||
run_unittest(__name__)
|
run_unittest(__name__)
|
||||||
|
|
|
@ -223,19 +223,19 @@ class FrameSummary:
|
||||||
- :attr:`line` The text from the linecache module for the
|
- :attr:`line` The text from the linecache module for the
|
||||||
of code that was running when the frame was captured.
|
of code that was running when the frame was captured.
|
||||||
- :attr:`locals` Either None if locals were not supplied, or a dict
|
- :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')
|
__slots__ = ('filename', 'lineno', 'name', '_line', 'locals')
|
||||||
|
|
||||||
def __init__(self, filename, lineno, name, lookup_line=True, locals=None,
|
def __init__(self, filename, lineno, name, *, lookup_line=True,
|
||||||
line=None):
|
locals=None, line=None):
|
||||||
"""Construct a FrameSummary.
|
"""Construct a FrameSummary.
|
||||||
|
|
||||||
:param lookup_line: If True, `linecache` is consulted for the source
|
:param lookup_line: If True, `linecache` is consulted for the source
|
||||||
code line. Otherwise, the line will be looked up when first needed.
|
code line. Otherwise, the line will be looked up when first needed.
|
||||||
:param locals: If supplied the frame locals, which will be captured as
|
: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
|
:param line: If provided, use this instead of looking up the line in
|
||||||
the linecache.
|
the linecache.
|
||||||
"""
|
"""
|
||||||
|
@ -246,7 +246,7 @@ class FrameSummary:
|
||||||
if lookup_line:
|
if lookup_line:
|
||||||
self.line
|
self.line
|
||||||
self.locals = \
|
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):
|
def __eq__(self, other):
|
||||||
return (self.filename == other.filename and
|
return (self.filename == other.filename and
|
||||||
|
@ -299,7 +299,8 @@ class StackSummary(list):
|
||||||
"""A stack of frames."""
|
"""A stack of frames."""
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Create a StackSummary from a traceback or stack object.
|
||||||
|
|
||||||
:param frame_gen: A generator that yields (frame, lineno) tuples to
|
:param frame_gen: A generator that yields (frame, lineno) tuples to
|
||||||
|
@ -308,6 +309,8 @@ class StackSummary(list):
|
||||||
include.
|
include.
|
||||||
:param lookup_lines: If True, lookup lines for each frame immediately,
|
:param lookup_lines: If True, lookup lines for each frame immediately,
|
||||||
otherwise lookup is deferred until the frame is rendered.
|
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:
|
if limit is None:
|
||||||
limit = getattr(sys, 'tracebacklimit', None)
|
limit = getattr(sys, 'tracebacklimit', None)
|
||||||
|
@ -324,7 +327,12 @@ class StackSummary(list):
|
||||||
fnames.add(filename)
|
fnames.add(filename)
|
||||||
linecache.lazycache(filename, f.f_globals)
|
linecache.lazycache(filename, f.f_globals)
|
||||||
# Must defer line lookups until we have called checkcache.
|
# 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:
|
for filename in fnames:
|
||||||
linecache.checkcache(filename)
|
linecache.checkcache(filename)
|
||||||
# If immediate lookup was desired, trigger lookups now.
|
# If immediate lookup was desired, trigger lookups now.
|
||||||
|
@ -356,11 +364,16 @@ class StackSummary(list):
|
||||||
newlines as well, for those items with source text lines.
|
newlines as well, for those items with source text lines.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for filename, lineno, name, line in self:
|
for frame in self:
|
||||||
item = ' File "{}", line {}, in {}\n'.format(filename, lineno, name)
|
row = []
|
||||||
if line:
|
row.append(' File "{}", line {}, in {}\n'.format(
|
||||||
item = item + ' {}\n'.format(line.strip())
|
frame.filename, frame.lineno, frame.name))
|
||||||
result.append(item)
|
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
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -392,8 +405,8 @@ class TracebackException:
|
||||||
- :attr:`msg` For syntax errors - the compiler error message.
|
- :attr:`msg` For syntax errors - the compiler error message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, exc_type, exc_value, exc_traceback, limit=None,
|
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
|
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
|
||||||
# permit backwards compat with the existing API, otherwise we
|
# permit backwards compat with the existing API, otherwise we
|
||||||
# need stub thunk objects just to glue it together.
|
# need stub thunk objects just to glue it together.
|
||||||
|
@ -411,6 +424,7 @@ class TracebackException:
|
||||||
exc_value.__cause__.__traceback__,
|
exc_value.__cause__.__traceback__,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
lookup_lines=False,
|
lookup_lines=False,
|
||||||
|
capture_locals=capture_locals,
|
||||||
_seen=_seen)
|
_seen=_seen)
|
||||||
else:
|
else:
|
||||||
cause = None
|
cause = None
|
||||||
|
@ -422,6 +436,7 @@ class TracebackException:
|
||||||
exc_value.__context__.__traceback__,
|
exc_value.__context__.__traceback__,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
lookup_lines=False,
|
lookup_lines=False,
|
||||||
|
capture_locals=capture_locals,
|
||||||
_seen=_seen)
|
_seen=_seen)
|
||||||
else:
|
else:
|
||||||
context = None
|
context = None
|
||||||
|
@ -431,7 +446,8 @@ class TracebackException:
|
||||||
exc_value.__suppress_context__ if exc_value else False
|
exc_value.__suppress_context__ if exc_value else False
|
||||||
# TODO: locals.
|
# TODO: locals.
|
||||||
self.stack = StackSummary.extract(
|
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
|
self.exc_type = exc_type
|
||||||
# Capture now to permit freeing resources: only complication is in the
|
# Capture now to permit freeing resources: only complication is in the
|
||||||
# unofficial API _format_final_exc_line
|
# unofficial API _format_final_exc_line
|
||||||
|
@ -512,7 +528,7 @@ class TracebackException:
|
||||||
msg = self.msg or "<no detail available>"
|
msg = self.msg or "<no detail available>"
|
||||||
yield "{}: {}\n".format(stype, msg)
|
yield "{}: {}\n".format(stype, msg)
|
||||||
|
|
||||||
def format(self, chain=True):
|
def format(self, *, chain=True):
|
||||||
"""Format the exception.
|
"""Format the exception.
|
||||||
|
|
||||||
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
|
If chain is not *True*, *__cause__* and *__context__* will not be formatted.
|
||||||
|
|
|
@ -39,6 +39,8 @@ Library
|
||||||
- Issue #21619: Popen objects no longer leave a zombie after exit in the with
|
- Issue #21619: Popen objects no longer leave a zombie after exit in the with
|
||||||
statement if the pipe was broken. Patch by Martin Panter.
|
statement if the pipe was broken. Patch by Martin Panter.
|
||||||
|
|
||||||
|
- Issue #22936: Make it possible to show local variables in tracebacks.
|
||||||
|
|
||||||
- Issue #15955: Add an option to limit the output size in bz2.decompress().
|
- Issue #15955: Add an option to limit the output size in bz2.decompress().
|
||||||
Patch by Nikolaus Rath.
|
Patch by Nikolaus Rath.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue