commit 6758c6cea6ddcde054a7da038c5605d97e606644 Author: Robert Collins Date: Tue Jan 27 12:10:12 2015 +1300 Issue #22936: Make it possible to show local variables in tracebacks. diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 585025d..39f3da2 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -159,17 +159,21 @@ The module also defines the following classes: :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, lookup_lines and + capture_locals=False are as for the :class:`.StackSummary` class. - Capture an exception for later rendering. limit and lookup_lines - 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 @@ capture data for later printing in a lightweight fashion. :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 @@ capture data for later printing in a lightweight fashion. 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 index 9ff7548..81d19b1 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 "/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 @@ 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}) + 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 index df90abb..3a98847 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -293,7 +293,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 @@ -302,6 +303,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 from will + be captured into the FrameSummary objects as strings. """ if limit is None: limit = getattr(sys, 'tracebacklimit', None) @@ -318,7 +321,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. @@ -350,11 +358,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 @@ -387,7 +400,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. @@ -405,6 +418,7 @@ class TracebackException: exc_value.__cause__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: cause = None @@ -416,6 +430,7 @@ class TracebackException: exc_value.__context__.__traceback__, limit=limit, lookup_lines=False, + capture_locals=capture_locals, _seen=_seen) else: context = None @@ -425,7 +440,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 diff --git a/Misc/NEWS b/Misc/NEWS index 40d8228..3df7a9f 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -396,6 +396,8 @@ Library - 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").