tbutils docstrings 90% done

This commit is contained in:
Mahmoud Hashemi 2015-04-02 20:16:00 -07:00
parent 8e55d2b676
commit e16a2b4bc5
1 changed files with 183 additions and 12 deletions

View File

@ -19,7 +19,6 @@ There is also the :class:`ContextualTracebackInfo` variant of
:class:`TracebackInfo`, which includes much more information from each
frame of the callstack, including values of locals and neighboring
lines of code.
"""
from __future__ import print_function
@ -100,10 +99,13 @@ class Callpoint(object):
@classmethod
def from_tb(cls, tb):
# main difference with from_frame is that lineno and lasti
# come from the traceback, which is to say the line that
# failed in the try block, not the line currently being
# executed (in the except block)
"""\
Create a Callpoint from the traceback of the current
exception. Main difference with :meth:`from_frame` is that
``lineno`` and ``lasti`` come from the traceback, which is to
say the line that failed in the try block, not the line
currently being executed (in the except block).
"""
func_name = tb.tb_frame.f_code.co_name
lineno = tb.tb_lineno
lasti = tb.tb_lasti
@ -122,6 +124,12 @@ class Callpoint(object):
return '%s(%s)' % (cn, ', '.join([repr(a) for a in args]))
def tb_frame_str(self):
"""\
Render the Callpoint as it would appear in a standard printed
Python traceback. Returns a string with filename, line number,
function name, and the actual code line of the error on up to
two lines.
"""
ret = ' File "%s", line %s, in %s\n' % (self.module_path,
self.lineno,
self.func_name)
@ -131,6 +139,20 @@ class Callpoint(object):
class _DeferredLine(object):
"""\
The _DeferredLine type allows Callpoints and TracebackInfos to be
constructed without potentially hitting the filesystem, as is the
normal behavior of the standard Python :mod:`traceback` and
:mod:`linecache` modules. Calling :func:`str` fetches and caches
the line.
Args:
filename (str): the path of the file containing the line
lineno (int): the number of the line in question
module_globals (dict): an optional dict of module globals,
used to handle advanced use cases using custom module loaders.
"""
def __init__(self, filename, lineno, module_globals=None):
self.filename = filename
self.lineno = lineno
@ -172,7 +194,17 @@ class _DeferredLine(object):
# TODO: dedup frames, look at __eq__ on _DeferredLine
class TracebackInfo(object):
"""
The TracebackInfo class provides a basic representation of a stack
trace, be it from an exception being handled or just part of
normal execution. It is basically a wrapper around a list of
:class:`Callpoint` objects representing frames.
Args:
frames (list): A list of frame objects in the stack.
"""
# TODO: from current and from current exc
callpoint_type = Callpoint
def __init__(self, frames):
@ -180,9 +212,19 @@ class TracebackInfo(object):
@classmethod
def from_frame(cls, frame, limit=None):
"""\
Create a new TracebackInfo from the frame passed by recurring up
in the stack up to *limit* times.
Args:
frame (types.FrameType): frame object from
:func:`sys._getframe` or elsewhere.
limit (int): max number of parent frames to extract
(defaults to :ref:`sys.tracebacklimit`)
"""
ret = []
if frame is None:
frame = sys._getframe(1) # cross-impl yadayada
frame = sys._getframe(1)
if limit is None:
limit = getattr(sys, 'tracebacklimit', 1000)
n = 0
@ -196,6 +238,16 @@ class TracebackInfo(object):
@classmethod
def from_traceback(cls, tb, limit=None):
"""\
Create a new TracebackInfo from the traceback passed by recurring up
in the stack up to *limit* times.
Args:
frame (types.FrameType): frame object from
:func:`sys.exc_info` or elsewhere.
limit (int): max number of parent frames to extract
(defaults to :ref:`sys.tracebacklimit`)
"""
ret = []
if limit is None:
limit = getattr(sys, 'tracebacklimit', 1000)
@ -209,9 +261,14 @@ class TracebackInfo(object):
@classmethod
def from_dict(cls, d):
"Complements :meth:`TracebackInfo.to_dict`."
# TODO: check this.
return cls(d['frames'])
def to_dict(self):
"""Returns a dict with a list of :class:`Callpoint` frames converted
to dicts.
"""
return {'frames': [f.to_dict() for f in self.frames]}
def __len__(self):
@ -234,12 +291,42 @@ class TracebackInfo(object):
return self.get_formatted()
def get_formatted(self):
"""\
Returns a string as formatted in the traditional Python
builtin style observable when an exception is not caught. In
other words, mimics :func:`traceback.format_tb` and
:func:`traceback.format_stack`.
"""
ret = 'Traceback (most recent call last):\n'
ret += ''.join([f.tb_frame_str() for f in self.frames])
return ret
class ExceptionInfo(object):
"""\
An ExceptionInfo object ties together three main fields suitable
for representing an instance of an exception: The exception type
name, a string representation of the exception itself (the
exception message), and information about the traceback (stored as
a :class:`TracebackInfo` object).
These fields line up with :func:`sys.exc_info`, but unlike the
values returned by that function, ExceptionInfo does not hold any
references to the real exception or traceback. This property makes
it suitable for serialization or long-term retention, without
worrying about formatting pitfalls, circular references, or leaking memory.
Args:
exc_type (str): The exception type name.
exc_msg (str): String representation of the exception value.
tb_info (TracebackInfo): Information about the stack trace of the
exception.
Like the TracebackInfo, ExceptionInfo is most commonly
instantiated from one of its classmethods: :meth:`from_exc_info`
or :meth:`from_current`.
"""
tb_info_type = TracebackInfo
@ -264,6 +351,10 @@ class ExceptionInfo(object):
return cls.from_exc_info(*sys.exc_info())
def to_dict(self):
"""\
Get a :class:`dict` representation of the ExceptionInfo, suitable
for JSON serialization.
"""
return {'exc_type': self.exc_type,
'exc_msg': self.exc_msg,
'exc_tb': self.tb_info.to_dict()}
@ -280,12 +371,25 @@ class ExceptionInfo(object):
return '<%s [%s: %s] (%s frames%s)>' % args
def get_formatted(self):
"""\
Returns a string formatted in the traditional Python
builtin style observable when an exception is not caught. In
other words, mimics :func:`traceback.format_exception`.
"""
# TODO: add SyntaxError formatting
tb_str = str(self.tb_info)
tb_str = self.tb_info.get_formatted()
return ''.join([tb_str, '%s: %s' % (self.exc_type, self.exc_msg)])
class ContextualCallpoint(Callpoint):
"""The ContextualCallpoint is a :class:`Callpoint` subtype with the
exact same API and storing two additional values:
1. :func:`repr` outputs for local variables from the Callpoint's scope
2. A number of lines before and after the Callpoint's line of code
The ContextualCallpoint is used by the :class:`ContextualTracebackInfo`.
"""
def __init__(self, *a, **kw):
self.local_reprs = kw.pop('local_reprs', {})
self.pre_lines = kw.pop('pre_lines', [])
@ -294,6 +398,7 @@ class ContextualCallpoint(Callpoint):
@classmethod
def from_frame(cls, frame):
"Identical to :meth:`Callpoint.from_frame`"
ret = super(ContextualCallpoint, cls).from_frame(frame)
ret._populate_local_reprs(frame.f_locals)
ret._populate_context_lines()
@ -301,6 +406,7 @@ class ContextualCallpoint(Callpoint):
@classmethod
def from_tb(cls, tb):
"Identical to :meth:`Callpoint.from_tb`"
ret = super(ContextualCallpoint, cls).from_tb(tb)
ret._populate_local_reprs(tb.tb_frame.f_locals)
ret._populate_context_lines()
@ -332,6 +438,24 @@ class ContextualCallpoint(Callpoint):
return
def to_dict(self):
"""
Same principle as :meth:`Callpoint.to_dict`, but with the added
contextual values. With ``ContextualCallpoint.to_dict()``,
each frame will now be represented like::
{'func_name': 'print_example',
'lineno': 0,
'module_name': 'example_module',
'module_path': '/home/example/example_module.pyc',
'lasti': 0,
'line': 'print "example"',
'locals': {'variable': '"value"'},
'pre_lines': ['variable = "value"'],
'post_lines': []}
The locals dictionary and line lists are copies and can be mutated
freely.
"""
ret = super(ContextualCallpoint, self).to_dict()
ret['locals'] = dict(self.local_reprs)
@ -361,10 +485,25 @@ class ContextualCallpoint(Callpoint):
class ContextualTracebackInfo(TracebackInfo):
"""\
The ContextualTracebackInfo type is a :class:`TracebackInfo`
subtype that is used by :class:`ContextualExceptionInfo` and uses
the :class:`ContextualCallpoint` as its frame-representing
primitive.
"""
callpoint_type = ContextualCallpoint
class ContextualExceptionInfo(ExceptionInfo):
"""\
The ContextualTracebackInfo type is a :class:`TracebackInfo`
subtype that uses the :class:`ContextualCallpoint` as its
frame-representing primitive.
It carries with it most of the exception information required to
recreate the widely recognizable "500" page for debugging Django
applications.
"""
tb_info_type = ContextualTracebackInfo
@ -464,6 +603,11 @@ def print_exception(etype, value, tb, limit=None, file=None):
def fix_print_exception():
"""
Sets the default exception hook :func:`sys.excepthook` to the
:func:`tbutils.print_exception` that uses all the ``tbutils``
facilities to provide slightly more correct output behavior.
"""
sys.excepthook = print_exception
@ -473,8 +617,15 @@ _se_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)')
class ParsedTB(object):
"""
Parses a traceback string as typically output by sys.excepthook.
"""\
Stores a parsed traceback and exception as would be typically
output by :func:`sys.excepthook` or
:func:`traceback.print_exception`.
.. note:
Does not currently store SyntaxError details such as column.
"""
def __init__(self, exc_type_name, exc_msg, frames=None):
self.exc_type = exc_type_name
@ -483,15 +634,20 @@ class ParsedTB(object):
@property
def source_file(self):
"""
The file path of module containing the function that raised the
exception, or None if not available.
"""
try:
return self.frames[-1]['filepath']
except IndexError:
return None
def to_dict(self):
"Get a copy as a JSON-serializable :class:`dict`"
return {'exc_type': self.exc_type,
'exc_msg': self.exc_msg,
'frames': self.frames}
'frames': self.frames} # TODO: copy?
def __repr__(self):
cn = self.__class__.__name__
@ -500,11 +656,24 @@ class ParsedTB(object):
@classmethod
def from_string(cls, tb_str):
"""Parse a traceback and exception from the text *tb_str*. This text
is expected to have been decoded, otherwise it will be
interpreted as UTF-8.
This method does not search a larger body of text for
tracebacks. If the first line of the text passed does not
match one of the known patterns, a :exc:`ValueError` will be
raised. This method will ignore trailing text after the end of
the first traceback.
Args:
tb_str (str): The traceback text (:class:`unicode` or UTF-8 bytes)
"""
if not isinstance(tb_str, unicode):
tb_str = tb_str.decode('utf-8')
tb_lines = tb_str.lstrip().splitlines()
# first off, handle some ignored exceptions. these can be the
# First off, handle some ignored exceptions. These can be the
# result of exceptions raised by __del__ during garbage
# collection
while tb_lines:
@ -513,11 +682,13 @@ class ParsedTB(object):
tb_lines.pop()
else:
break
if tb_lines and tb_lines[0].strip() == 'Traceback (most recent call last):':
start_line = 1
frame_re = _frame_re
elif len(tb_lines) > 1 and tb_lines[-2].lstrip().startswith('^'):
# This is to handle the slight formatting difference
# associated with SyntaxErrors, which also don't really
# have tracebacks
start_line = 0
frame_re = _se_frame_re
else: