diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -159,17 +159,21 @@ :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) +.. 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 - the :class:`.StackSummary` class. + Capture an exception for later rendering. limit, lookup_lines and + 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 -.. 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 - are as for the :class:`.StackSummary` class. + Capture an exception for later rendering. limit, lookup_lines and + 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 @@ -227,7 +231,7 @@ :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 `walk_stack` or `walk_tb`. @@ -236,6 +240,8 @@ 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 may be valuable if it may not actually get formatted). + If capture_locals is True the local variables in each *FrameSummary* are + captured as strings. .. versionadded:: 3.5 diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py --- 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 "/home/robertc/work/cpython/Lib/test/test_traceback.py", 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): @@ -665,13 +696,31 @@ 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}) + tb = test_tb(f, 6, None) + exc = traceback.TracebackException(Exception, e, tb, capture_locals=True) + self.assertEqual(exc.stack[0].locals, {'something': '1'}) + + 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 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -293,7 +293,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 @@ -302,6 +303,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 from will + be captured into the FrameSummary objects as strings. """ if limit is None: limit = getattr(sys, 'tracebacklimit', None) @@ -318,7 +321,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. @@ -350,11 +358,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 @@ -387,7 +400,7 @@ """ 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. @@ -405,6 +418,7 @@ exc_value.__cause__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: cause = None @@ -416,6 +430,7 @@ exc_value.__context__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: context = None @@ -425,7 +440,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 diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -396,6 +396,8 @@ - Issue #19361: JSON decoder now raises JSONDecodeError instead of ValueError. +- Issue #22936: Make it possible to show local variables in tracebacks. + - Issue #18518: timeit now rejects statements which can't be compiled outside a function or a loop (e.g. "return" or "break").