diff --git a/boltons/tbutils.py b/boltons/tbutils.py index 0caf035..97a42d8 100644 --- a/boltons/tbutils.py +++ b/boltons/tbutils.py @@ -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.+)", line (?P\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: