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 :class:`TracebackInfo`, which includes much more information from each
frame of the callstack, including values of locals and neighboring frame of the callstack, including values of locals and neighboring
lines of code. lines of code.
""" """
from __future__ import print_function from __future__ import print_function
@ -100,10 +99,13 @@ class Callpoint(object):
@classmethod @classmethod
def from_tb(cls, tb): 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 Create a Callpoint from the traceback of the current
# failed in the try block, not the line currently being exception. Main difference with :meth:`from_frame` is that
# executed (in the except block) ``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 func_name = tb.tb_frame.f_code.co_name
lineno = tb.tb_lineno lineno = tb.tb_lineno
lasti = tb.tb_lasti lasti = tb.tb_lasti
@ -122,6 +124,12 @@ class Callpoint(object):
return '%s(%s)' % (cn, ', '.join([repr(a) for a in args])) return '%s(%s)' % (cn, ', '.join([repr(a) for a in args]))
def tb_frame_str(self): 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, ret = ' File "%s", line %s, in %s\n' % (self.module_path,
self.lineno, self.lineno,
self.func_name) self.func_name)
@ -131,6 +139,20 @@ class Callpoint(object):
class _DeferredLine(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): def __init__(self, filename, lineno, module_globals=None):
self.filename = filename self.filename = filename
self.lineno = lineno self.lineno = lineno
@ -172,7 +194,17 @@ class _DeferredLine(object):
# TODO: dedup frames, look at __eq__ on _DeferredLine # TODO: dedup frames, look at __eq__ on _DeferredLine
class TracebackInfo(object): 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 callpoint_type = Callpoint
def __init__(self, frames): def __init__(self, frames):
@ -180,9 +212,19 @@ class TracebackInfo(object):
@classmethod @classmethod
def from_frame(cls, frame, limit=None): 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 = [] ret = []
if frame is None: if frame is None:
frame = sys._getframe(1) # cross-impl yadayada frame = sys._getframe(1)
if limit is None: if limit is None:
limit = getattr(sys, 'tracebacklimit', 1000) limit = getattr(sys, 'tracebacklimit', 1000)
n = 0 n = 0
@ -196,6 +238,16 @@ class TracebackInfo(object):
@classmethod @classmethod
def from_traceback(cls, tb, limit=None): 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 = [] ret = []
if limit is None: if limit is None:
limit = getattr(sys, 'tracebacklimit', 1000) limit = getattr(sys, 'tracebacklimit', 1000)
@ -209,9 +261,14 @@ class TracebackInfo(object):
@classmethod @classmethod
def from_dict(cls, d): def from_dict(cls, d):
"Complements :meth:`TracebackInfo.to_dict`."
# TODO: check this.
return cls(d['frames']) return cls(d['frames'])
def to_dict(self): 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]} return {'frames': [f.to_dict() for f in self.frames]}
def __len__(self): def __len__(self):
@ -234,12 +291,42 @@ class TracebackInfo(object):
return self.get_formatted() return self.get_formatted()
def get_formatted(self): 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 = 'Traceback (most recent call last):\n'
ret += ''.join([f.tb_frame_str() for f in self.frames]) ret += ''.join([f.tb_frame_str() for f in self.frames])
return ret return ret
class ExceptionInfo(object): 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 tb_info_type = TracebackInfo
@ -264,6 +351,10 @@ class ExceptionInfo(object):
return cls.from_exc_info(*sys.exc_info()) return cls.from_exc_info(*sys.exc_info())
def to_dict(self): def to_dict(self):
"""\
Get a :class:`dict` representation of the ExceptionInfo, suitable
for JSON serialization.
"""
return {'exc_type': self.exc_type, return {'exc_type': self.exc_type,
'exc_msg': self.exc_msg, 'exc_msg': self.exc_msg,
'exc_tb': self.tb_info.to_dict()} 'exc_tb': self.tb_info.to_dict()}
@ -280,12 +371,25 @@ class ExceptionInfo(object):
return '<%s [%s: %s] (%s frames%s)>' % args return '<%s [%s: %s] (%s frames%s)>' % args
def get_formatted(self): 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 # 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)]) return ''.join([tb_str, '%s: %s' % (self.exc_type, self.exc_msg)])
class ContextualCallpoint(Callpoint): 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): def __init__(self, *a, **kw):
self.local_reprs = kw.pop('local_reprs', {}) self.local_reprs = kw.pop('local_reprs', {})
self.pre_lines = kw.pop('pre_lines', []) self.pre_lines = kw.pop('pre_lines', [])
@ -294,6 +398,7 @@ class ContextualCallpoint(Callpoint):
@classmethod @classmethod
def from_frame(cls, frame): def from_frame(cls, frame):
"Identical to :meth:`Callpoint.from_frame`"
ret = super(ContextualCallpoint, cls).from_frame(frame) ret = super(ContextualCallpoint, cls).from_frame(frame)
ret._populate_local_reprs(frame.f_locals) ret._populate_local_reprs(frame.f_locals)
ret._populate_context_lines() ret._populate_context_lines()
@ -301,6 +406,7 @@ class ContextualCallpoint(Callpoint):
@classmethod @classmethod
def from_tb(cls, tb): def from_tb(cls, tb):
"Identical to :meth:`Callpoint.from_tb`"
ret = super(ContextualCallpoint, cls).from_tb(tb) ret = super(ContextualCallpoint, cls).from_tb(tb)
ret._populate_local_reprs(tb.tb_frame.f_locals) ret._populate_local_reprs(tb.tb_frame.f_locals)
ret._populate_context_lines() ret._populate_context_lines()
@ -332,6 +438,24 @@ class ContextualCallpoint(Callpoint):
return return
def to_dict(self): 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 = super(ContextualCallpoint, self).to_dict()
ret['locals'] = dict(self.local_reprs) ret['locals'] = dict(self.local_reprs)
@ -361,10 +485,25 @@ class ContextualCallpoint(Callpoint):
class ContextualTracebackInfo(TracebackInfo): 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 callpoint_type = ContextualCallpoint
class ContextualExceptionInfo(ExceptionInfo): 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 tb_info_type = ContextualTracebackInfo
@ -464,6 +603,11 @@ def print_exception(etype, value, tb, limit=None, file=None):
def fix_print_exception(): 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 sys.excepthook = print_exception
@ -473,8 +617,15 @@ _se_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)')
class ParsedTB(object): 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): def __init__(self, exc_type_name, exc_msg, frames=None):
self.exc_type = exc_type_name self.exc_type = exc_type_name
@ -483,15 +634,20 @@ class ParsedTB(object):
@property @property
def source_file(self): def source_file(self):
"""
The file path of module containing the function that raised the
exception, or None if not available.
"""
try: try:
return self.frames[-1]['filepath'] return self.frames[-1]['filepath']
except IndexError: except IndexError:
return None return None
def to_dict(self): def to_dict(self):
"Get a copy as a JSON-serializable :class:`dict`"
return {'exc_type': self.exc_type, return {'exc_type': self.exc_type,
'exc_msg': self.exc_msg, 'exc_msg': self.exc_msg,
'frames': self.frames} 'frames': self.frames} # TODO: copy?
def __repr__(self): def __repr__(self):
cn = self.__class__.__name__ cn = self.__class__.__name__
@ -500,11 +656,24 @@ class ParsedTB(object):
@classmethod @classmethod
def from_string(cls, tb_str): 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): if not isinstance(tb_str, unicode):
tb_str = tb_str.decode('utf-8') tb_str = tb_str.decode('utf-8')
tb_lines = tb_str.lstrip().splitlines() 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 # result of exceptions raised by __del__ during garbage
# collection # collection
while tb_lines: while tb_lines:
@ -513,11 +682,13 @@ class ParsedTB(object):
tb_lines.pop() tb_lines.pop()
else: else:
break break
if tb_lines and tb_lines[0].strip() == 'Traceback (most recent call last):': if tb_lines and tb_lines[0].strip() == 'Traceback (most recent call last):':
start_line = 1 start_line = 1
frame_re = _frame_re frame_re = _frame_re
elif len(tb_lines) > 1 and tb_lines[-2].lstrip().startswith('^'): 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 start_line = 0
frame_re = _se_frame_re frame_re = _se_frame_re
else: else: