diff options
author | Robert Collins <rbtcollins@hp.com> | 2015-03-05 07:28:52 (GMT) |
---|---|---|
committer | Robert Collins <rbtcollins@hp.com> | 2015-03-05 07:28:52 (GMT) |
commit | d7c7e0ef69e0aacc24d34388dd68927f7f7ee1f3 (patch) | |
tree | ebb63d731b8e9d84d4285b59b4e970cf509e9eea /Lib | |
parent | 2856332f5efa28c2abf9805cfdfdbf10b733b231 (diff) | |
download | cpython-d7c7e0ef69e0aacc24d34388dd68927f7f7ee1f3.zip cpython-d7c7e0ef69e0aacc24d34388dd68927f7f7ee1f3.tar.gz cpython-d7c7e0ef69e0aacc24d34388dd68927f7f7ee1f3.tar.bz2 |
Issue #22936: Make it possible to show local variables in tracebacks.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/test/test_traceback.py | 68 | ||||
-rw-r--r-- | Lib/traceback.py | 48 |
2 files changed, 92 insertions, 24 deletions
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 @@ import traceback 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 @@ class TestStack(unittest.TestCase): 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 @@ 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, 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 @@ 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 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 @@ 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__) @@ -664,13 +696,33 @@ 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, 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 @@ 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. """ @@ -246,7 +246,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 @@ -299,7 +299,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 @@ -308,6 +309,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) @@ -324,7 +327,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. @@ -356,11 +364,16 @@ class StackSummary(list): 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 @@ class TracebackException: - :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 @@ class TracebackException: exc_value.__cause__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: cause = None @@ -422,6 +436,7 @@ class TracebackException: exc_value.__context__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: context = None @@ -431,7 +446,8 @@ class TracebackException: 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 @@ class TracebackException: 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. |