[Python-checkins] cpython: Issue #22936: Make it possible to show local variables in tracebacks.

robert.collins python-checkins at python.org
Thu Mar 5 08:29:15 CET 2015


https://hg.python.org/cpython/rev/50741316dd3a
changeset:   94867:50741316dd3a
user:        Robert Collins <rbtcollins at hp.com>
date:        Thu Mar 05 20:28:52 2015 +1300
summary:
  Issue #22936: Make it possible to show local variables in tracebacks.

files:
  Doc/library/traceback.rst  |  28 ++++++---
  Lib/test/test_traceback.py |  68 ++++++++++++++++++++++---
  Lib/traceback.py           |  48 ++++++++++++------
  Misc/NEWS                  |   2 +
  4 files changed, 112 insertions(+), 34 deletions(-)


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
 
@@ -190,7 +194,7 @@
                error occured.
 .. attribute:: `.msg` For syntax errors - the compiler error message.
 
-.. method::  TracebackException.format(chain=True)
+.. method::  TracebackException.format(*, chain=True)
 
     Format the exception.
 
@@ -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 object representations.
 
    .. versionadded:: 3.5
 
@@ -258,8 +264,10 @@
    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
    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
-   will prevent line lookups happening at all.
+   also happens when casting it to a tuple). *line* may be directly provided, and
+   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:
 
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 "' + __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 @@
         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 @@
         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
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -223,19 +223,19 @@
     - :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 @@
         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 @@
     """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 @@
             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 @@
             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 @@
         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 @@
     - :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 @@
                 exc_value.__cause__.__traceback__,
                 limit=limit,
                 lookup_lines=False,
+                capture_locals=capture_locals,
                 _seen=_seen)
         else:
             cause = None
@@ -422,6 +436,7 @@
                 exc_value.__context__.__traceback__,
                 limit=limit,
                 lookup_lines=False,
+                capture_locals=capture_locals,
                 _seen=_seen)
         else:
             context = None
@@ -431,7 +446,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
@@ -512,7 +528,7 @@
         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.
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -39,6 +39,8 @@
 - Issue #21619: Popen objects no longer leave a zombie after exit in the with
   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().
   Patch by Nikolaus Rath.
 

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list