Skip to content

Commit

Permalink
Improve the tracebacks returned by Failures
Browse files Browse the repository at this point in the history
Include the call site as well as the exception path in the traceback objects
returned by Failure.getTracebackObject. This can be useful for debugging.
  • Loading branch information
richvdh committed Oct 9, 2017
1 parent 7560c87 commit edf2704
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 24 deletions.
64 changes: 42 additions & 22 deletions src/twisted/python/failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,22 +90,42 @@ class NoCurrentExceptionError(Exception):



def _Traceback(frames):
def _Traceback(stack_frames, tb_frames):
"""
Construct a fake traceback object using a list of frames. Note that
although frames generally include locals and globals, this information
is not kept by this method, since locals and globals are not used in
standard tracebacks.
@param frames: [(methodname, filename, lineno, locals, globals), ...]
@param stack_frames: [(methodname, filename, lineno, locals, globals), ...]
@param tb_frames: [(methodname, filename, lineno, locals, globals), ...]
"""
assert len(frames) > 0, "Must pass some frames"
assert len(tb_frames) > 0, "Must pass some frames"
# We deliberately avoid using recursion here, as the frames list may be
# long.
tb = None
for frame in reversed(frames):
tb = _TracebackFrame(frame, tb)
return tb

# 'stack_frames' is a list of frames above (ie, older than) the point the
# exception was caught, with oldest at the start. Start by building these
# into a linked list of _Frame objects (with the f_back links pointing back
# towards the oldest frame).
stack = None
for sf in stack_frames:
stack = _Frame(sf, stack)

# 'tb_frames' is a list of frames from the point the exception was caught,
# down to where it was thrown, with the oldest at the start. Add these to
# the linked list of _Frames, but also wrap each one with a _Traceback
# frame which is linked in the opposite direction (towards the newest
# frame).
stack = _Frame(tb_frames[0], stack)
first_tb = tb = _TracebackFrame(stack)
for sf in tb_frames[1:]:
stack = _Frame(sf, stack)
tb.tb_next = _TracebackFrame(stack)
tb = tb.tb_next

# return the first _TracebackFrame.
return first_tb



Expand All @@ -115,38 +135,38 @@ class _TracebackFrame(object):
library L{traceback} module.
"""

def __init__(self, frame, tb_next):
def __init__(self, frame):
"""
@param frame: (methodname, filename, lineno, locals, globals)
@param tb_next: next inner _TracebackFrame object, or None if this
is the innermost frame.
@param frame: _Frame object
"""
name, filename, lineno, localz, globalz = frame
self.tb_frame = _Frame(name, filename)
self.tb_lineno = lineno
self.tb_next = tb_next

self.tb_frame = frame
self.tb_lineno = frame.f_lineno
self.tb_next = None


class _Frame(object):
"""
A fake frame object, used by L{_Traceback}.
@ivar f_code: fake L{code<types.CodeType>} object
@ivar f_lineno: line number
@ivar f_globals: fake f_globals dictionary (usually empty)
@ivar f_locals: fake f_locals dictionary (usually empty)
@ivar f_back: previous stack frame (towards the caller)
"""

def __init__(self, name, filename):
def __init__(self, frameinfo, back):
"""
@param name: method/function name for this frame.
@type name: C{str}
@param filename: filename for this frame.
@type name: C{str}
@param frameinfo: (methodname, filename, lineno, locals, globals)
@param back: previous (older) stack frame
@type back: C{frame}
"""
name, filename, lineno, localz, globalz = frameinfo
self.f_code = _Code(name, filename)
self.f_lineno = lineno
self.f_globals = {}
self.f_locals = {}
self.f_back = back



Expand Down Expand Up @@ -547,7 +567,7 @@ def getTracebackObject(self):
if self.tb is not None:
return self.tb
elif len(self.frames) > 0:
return _Traceback(self.frames)
return _Traceback(self.stack, self.frames)
else:
return None

Expand Down
14 changes: 12 additions & 2 deletions src/twisted/test/test_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ def test_singleFrame(self):
to be passed to L{traceback.extract_tb}, and we should get a singleton
list containing a (filename, lineno, methodname, line) tuple.
"""
tb = failure._Traceback([['method', 'filename.py', 123, {}, {}]])
tb = failure._Traceback([], [['method', 'filename.py', 123, {}, {}]])
# Note that we don't need to test that extract_tb correctly extracts
# the line's contents. In this case, since filename.py doesn't exist,
# it will just use None.
Expand All @@ -766,12 +766,22 @@ def test_manyFrames(self):
containing a tuple for each frame.
"""
tb = failure._Traceback([
['caller1', 'filename.py', 7, {}, {}],
['caller2', 'filename.py', 8, {}, {}],
], [
['method1', 'filename.py', 123, {}, {}],
['method2', 'filename.py', 235, {}, {}]])
['method2', 'filename.py', 235, {}, {}],
])
self.assertEqual(traceback.extract_tb(tb),
[_tb('filename.py', 123, 'method1', None),
_tb('filename.py', 235, 'method2', None)])

# we should also be able to extract_stack on it
self.assertEqual(traceback.extract_stack(tb.tb_frame),
[_tb('filename.py', 7, 'caller1', None),
_tb('filename.py', 8, 'caller2', None),
_tb('filename.py', 123, 'method1', None),
])


class FrameAttributesTests(SynchronousTestCase):
Expand Down

0 comments on commit edf2704

Please sign in to comment.