commit 0f7304e5d888c39917f298a0ece9dcc90ae7e355 Author: Robert Collins Date: Mon Jan 19 14:26:01 2015 +1300 Issue #17911: Make a Frame object for Traceback. This is part of a series of refactorings to provide a new API for traceback that permits complex operations such as rendering local variables, deferring expensive operations like statting and reading files off of disk until its actually needed. diff --git a/Doc/library/traceback.rst b/Doc/library/traceback.rst index 15fbedc..b7dcb2e 100644 --- a/Doc/library/traceback.rst +++ b/Doc/library/traceback.rst @@ -136,6 +136,14 @@ The module defines the following functions: .. versionadded:: 3.4 +.. class:: Frame(filename, lineno, name, lookup_line=True, locals=None) + :noindex: + + Represents a single frame in the traceback or stack that is being formatted + 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 Frame has the :attr:`line` attribute accessed (which + also happens when casting it to a tuple). .. _traceback-example: diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 6bd6fa6..ee48ff3 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1,6 +1,7 @@ """Test cases for traceback module""" from io import StringIO +import linecache import sys import unittest import re @@ -477,6 +478,27 @@ class MiscTracebackCases(unittest.TestCase): self.assertEqual(len(inner_frame.f_locals), 0) +class TestFrame(unittest.TestCase): + + def test_basics(self): + linecache.clearcache() + linecache.deferredcache("f", globals()) + f = traceback.Frame("f", 1, "dummy") + self.assertEqual( + ("f", 1, "dummy", '"""Test cases for traceback module"""'), + tuple(f)) + self.assertEqual(None, f.locals) + + def test_lazy_lines(self): + linecache.clearcache() + f = traceback.Frame("f", 1, "dummy", lookup_line=False) + self.assertEqual(None, f._line) + linecache.deferredcache("f", globals()) + self.assertEqual( + '"""Test cases for traceback module"""', + f.line) + + def test_main(): run_unittest(__name__) diff --git a/Lib/traceback.py b/Lib/traceback.py index c1ab36e..686bd80 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -62,14 +62,9 @@ def _extract_tb_or_stack_iter(curr, limit, extractor): name = co.co_name linecache.checkcache(filename) - line = linecache.getline(filename, lineno, f.f_globals) + line = linecache.deferredcache(filename, f.f_globals) - if line: - line = line.strip() - else: - line = None - - yield (filename, lineno, name, line) + yield Frame(filename, lineno, name) curr = next_item n += 1 @@ -311,3 +306,46 @@ def clear_frames(tb): # Ignore the exception raised if the frame is still executing. pass tb = tb.tb_next + + +class Frame: + """A single frame from a traceback. + + - :attr:`filename` The filename for the frame. + - :attr:`lineno` The line within filename for the frame that was + active when the frame was captured. + - :attr:`name` The name of the function or method that was executing + when the frame was captured. + - :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. + """ + + __slots__ = ('filename', 'lineno', 'name', '_line', 'locals') + + def __init__(self, filename, lineno, name, lookup_line=True, locals=None): + """Construct a Frame. + + :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. + """ + self.filename = filename + self.lineno = lineno + self.name = name + self._line = None + if lookup_line: + self.line + self.locals = \ + dict((k, str(v)) for k, v in locals.items()) if locals else None + + def __iter__(self): + return iter([self.filename, self.lineno, self.name, self.line]) + + @property + def line(self): + if self._line is None: + self._line = linecache.getline(self.filename, self.lineno).strip() + return self._line