123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827 |
- # -*- coding: utf-8 -*-
- # Copyright (c) 2013, Mahmoud Hashemi
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are
- # met:
- #
- # * Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- #
- # * Redistributions in binary form must reproduce the above
- # copyright notice, this list of conditions and the following
- # disclaimer in the documentation and/or other materials provided
- # with the distribution.
- #
- # * The names of the contributors may not be used to endorse or
- # promote products derived from this software without specific
- # prior written permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- """One of the oft-cited tenets of Python is that it is better to ask
- forgiveness than permission. That is, there are many cases where it is
- more inclusive and correct to handle exceptions than spend extra lines
- and execution time checking for conditions. This philosophy makes good
- exception handling features all the more important. Unfortunately
- Python's :mod:`traceback` module is woefully behind the times.
- The ``tbutils`` module provides two disparate but complementary featuresets:
- 1. With :class:`ExceptionInfo` and :class:`TracebackInfo`, the
- ability to extract, construct, manipulate, format, and serialize
- exceptions, tracebacks, and callstacks.
- 2. With :class:`ParsedException`, the ability to find and parse tracebacks
- from captured output such as logs and stdout.
- 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
- import re
- import sys
- import linecache
- try:
- text = unicode # Python 2
- except NameError:
- text = str # Python 3
- # TODO: chaining primitives? what are real use cases where these help?
- # TODO: print_* for backwards compatibility
- # __all__ = ['extract_stack', 'extract_tb', 'format_exception',
- # 'format_exception_only', 'format_list', 'format_stack',
- # 'format_tb', 'print_exc', 'format_exc', 'print_exception',
- # 'print_last', 'print_stack', 'print_tb']
- __all__ = ['ExceptionInfo', 'TracebackInfo', 'Callpoint',
- 'ContextualExceptionInfo', 'ContextualTracebackInfo',
- 'ContextualCallpoint', 'print_exception', 'ParsedException']
- class Callpoint(object):
- """The Callpoint is a lightweight object used to represent a single
- entry in the code of a call stack. It stores the code-related
- metadata of a given frame. Available attributes are the same as
- the parameters below.
- Args:
- func_name (str): the function name
- lineno (int): the line number
- module_name (str): the module name
- module_path (str): the filesystem path of the module
- lasti (int): the index of bytecode execution
- line (str): the single-line code content (if available)
- """
- __slots__ = ('func_name', 'lineno', 'module_name', 'module_path', 'lasti',
- 'line')
- def __init__(self, module_name, module_path, func_name,
- lineno, lasti, line=None):
- self.func_name = func_name
- self.lineno = lineno
- self.module_name = module_name
- self.module_path = module_path
- self.lasti = lasti
- self.line = line
- def to_dict(self):
- "Get a :class:`dict` copy of the Callpoint. Useful for serialization."
- ret = {}
- for slot in self.__slots__:
- try:
- val = getattr(self, slot)
- except AttributeError:
- pass
- else:
- ret[slot] = str(val) if isinstance(val, _DeferredLine) else val
- return ret
- @classmethod
- def from_current(cls, level=1):
- "Creates a Callpoint from the location of the calling function."
- frame = sys._getframe(level)
- return cls.from_frame(frame)
- @classmethod
- def from_frame(cls, frame):
- "Create a Callpoint object from data extracted from the given frame."
- func_name = frame.f_code.co_name
- lineno = frame.f_lineno
- module_name = frame.f_globals.get('__name__', '')
- module_path = frame.f_code.co_filename
- lasti = frame.f_lasti
- line = _DeferredLine(module_path, lineno, frame.f_globals)
- return cls(module_name, module_path, func_name,
- lineno, lasti, line=line)
- @classmethod
- def from_tb(cls, tb):
- """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
- module_name = tb.tb_frame.f_globals.get('__name__', '')
- module_path = tb.tb_frame.f_code.co_filename
- line = _DeferredLine(module_path, lineno, tb.tb_frame.f_globals)
- return cls(module_name, module_path, func_name,
- lineno, lasti, line=line)
- def __repr__(self):
- cn = self.__class__.__name__
- args = [getattr(self, s, None) for s in self.__slots__]
- if not any(args):
- return super(Callpoint, self).__repr__()
- else:
- 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)
- if self.line:
- ret += ' %s\n' % (str(self.line).strip(),)
- return ret
- 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.
- """
- __slots__ = ('filename', 'lineno', '_line', '_mod_name', '_mod_loader')
- def __init__(self, filename, lineno, module_globals=None):
- self.filename = filename
- self.lineno = lineno
- # TODO: this is going away when we fix linecache
- # TODO: (mark) read about loader
- if module_globals is None:
- self._mod_name = None
- self._mod_loader = None
- else:
- self._mod_name = module_globals.get('__name__')
- self._mod_loader = module_globals.get('__loader__')
- def __eq__(self, other):
- return (self.lineno, self.filename) == (other.lineno, other.filename)
- def __ne__(self, other):
- return not self == other
- def __str__(self):
- ret = getattr(self, '_line', None)
- if ret is not None:
- return ret
- try:
- linecache.checkcache(self.filename)
- mod_globals = {'__name__': self._mod_name,
- '__loader__': self._mod_loader}
- line = linecache.getline(self.filename,
- self.lineno,
- mod_globals)
- line = line.rstrip()
- except KeyError:
- line = ''
- self._line = line
- return line
- def __repr__(self):
- return repr(str(self))
- def __len__(self):
- return len(str(self))
- # 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.
- .. note ::
- ``TracebackInfo`` can represent both exception tracebacks and
- non-exception tracebacks (aka stack traces). As a result, there
- is no ``TracebackInfo.from_current()``, as that would be
- ambiguous. Instead, call :meth:`TracebackInfo.from_frame`
- without the *frame* argument for a stack trace, or
- :meth:`TracebackInfo.from_traceback` without the *tb* argument
- for an exception traceback.
- """
- callpoint_type = Callpoint
- def __init__(self, frames):
- self.frames = frames
- @classmethod
- def from_frame(cls, frame=None, level=1, limit=None):
- """Create a new TracebackInfo *frame* by recurring up in the stack a
- max of *limit* times. If *frame* is unset, get the frame from
- :func:`sys._getframe` using *level*.
- Args:
- frame (types.FrameType): frame object from
- :func:`sys._getframe` or elsewhere. Defaults to result
- of :func:`sys.get_frame`.
- level (int): If *frame* is unset, the desired frame is
- this many levels up the stack from the invocation of
- this method. Default ``1`` (i.e., caller of this method).
- limit (int): max number of parent frames to extract
- (defaults to :data:`sys.tracebacklimit`)
- """
- ret = []
- if frame is None:
- frame = sys._getframe(level)
- if limit is None:
- limit = getattr(sys, 'tracebacklimit', 1000)
- n = 0
- while frame is not None and n < limit:
- item = cls.callpoint_type.from_frame(frame)
- ret.append(item)
- frame = frame.f_back
- n += 1
- ret.reverse()
- return cls(ret)
- @classmethod
- def from_traceback(cls, tb=None, limit=None):
- """Create a new TracebackInfo from the traceback *tb* by recurring
- up in the stack a max of *limit* times. If *tb* is unset, get
- the traceback from the currently handled exception. If no
- exception is being handled, raise a :exc:`ValueError`.
- Args:
- frame (types.TracebackType): traceback object from
- :func:`sys.exc_info` or elsewhere. If absent or set to
- ``None``, defaults to ``sys.exc_info()[2]``, and
- raises a :exc:`ValueError` if no exception is
- currently being handled.
- limit (int): max number of parent frames to extract
- (defaults to :data:`sys.tracebacklimit`)
- """
- ret = []
- if tb is None:
- tb = sys.exc_info()[2]
- if tb is None:
- raise ValueError('no tb set and no exception being handled')
- if limit is None:
- limit = getattr(sys, 'tracebacklimit', 1000)
- n = 0
- while tb is not None and n < limit:
- item = cls.callpoint_type.from_tb(tb)
- ret.append(item)
- tb = tb.tb_next
- n += 1
- return cls(ret)
- @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):
- return len(self.frames)
- def __iter__(self):
- return iter(self.frames)
- def __repr__(self):
- cn = self.__class__.__name__
- if self.frames:
- frame_part = ' last=%r' % (self.frames[-1],)
- else:
- frame_part = ''
- return '<%s frames=%s%s>' % (cn, len(self.frames), frame_part)
- def __str__(self):
- return self.get_formatted()
- def get_formatted(self):
- """Returns a string as formatted in the traditional Python
- built-in 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 :class:`TracebackInfo`, ExceptionInfo is most commonly
- instantiated from one of its classmethods: :meth:`from_exc_info`
- or :meth:`from_current`.
- """
- #: Override this in inherited types to control the TracebackInfo type used
- tb_info_type = TracebackInfo
- def __init__(self, exc_type, exc_msg, tb_info):
- # TODO: additional fields for SyntaxErrors
- self.exc_type = exc_type
- self.exc_msg = exc_msg
- self.tb_info = tb_info
- @classmethod
- def from_exc_info(cls, exc_type, exc_value, traceback):
- """Create an :class:`ExceptionInfo` object from the exception's type,
- value, and traceback, as returned by :func:`sys.exc_info`. See
- also :meth:`from_current`.
- """
- type_str = exc_type.__name__
- type_mod = exc_type.__module__
- if type_mod not in ("__main__", "__builtin__", "exceptions", "builtins"):
- type_str = '%s.%s' % (type_mod, type_str)
- val_str = _some_str(exc_value)
- tb_info = cls.tb_info_type.from_traceback(traceback)
- return cls(type_str, val_str, tb_info)
- @classmethod
- def from_current(cls):
- """Create an :class:`ExceptionInfo` object from the current exception
- being handled, by way of :func:`sys.exc_info`. Will raise an
- exception if no exception is currently being handled.
- """
- 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()}
- def __repr__(self):
- cn = self.__class__.__name__
- try:
- len_frames = len(self.tb_info.frames)
- last_frame = ', last=%r' % (self.tb_info.frames[-1],)
- except Exception:
- len_frames = 0
- last_frame = ''
- args = (cn, self.exc_type, self.exc_msg, len_frames, last_frame)
- return '<%s [%s: %s] (%s frames%s)>' % args
- def get_formatted(self):
- """Returns a string formatted in the traditional Python
- built-in style observable when an exception is not caught. In
- other words, mimics :func:`traceback.format_exception`.
- """
- # TODO: add SyntaxError formatting
- tb_str = self.tb_info.get_formatted()
- return ''.join([tb_str, '%s: %s' % (self.exc_type, self.exc_msg)])
- def get_formatted_exception_only(self):
- return '%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', [])
- self.post_lines = kw.pop('post_lines', [])
- super(ContextualCallpoint, self).__init__(*a, **kw)
- @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()
- return ret
- @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()
- return ret
- def _populate_context_lines(self, pivot=8):
- DL, lineno = _DeferredLine, self.lineno
- try:
- module_globals = self.line.module_globals
- except Exception:
- module_globals = None
- start_line = max(0, lineno - pivot)
- pre_lines = [DL(self.module_path, ln, module_globals)
- for ln in range(start_line, lineno)]
- self.pre_lines[:] = pre_lines
- post_lines = [DL(self.module_path, ln, module_globals)
- for ln in range(lineno + 1, lineno + 1 + pivot)]
- self.post_lines[:] = post_lines
- return
- def _populate_local_reprs(self, f_locals):
- local_reprs = self.local_reprs
- for k, v in f_locals.items():
- try:
- local_reprs[k] = repr(v)
- except Exception:
- surrogate = '<unprintable %s object>' % type(v).__name__
- local_reprs[k] = surrogate
- 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)
- # get the line numbers and textual lines
- # without assuming DeferredLines
- start_line = self.lineno - len(self.pre_lines)
- pre_lines = [{'lineno': start_line + i, 'line': str(l)}
- for i, l in enumerate(self.pre_lines)]
- # trim off leading empty lines
- for i, item in enumerate(pre_lines):
- if item['line']:
- break
- if i:
- pre_lines = pre_lines[i:]
- ret['pre_lines'] = pre_lines
- # now post_lines
- post_lines = [{'lineno': self.lineno + i, 'line': str(l)}
- for i, l in enumerate(self.post_lines)]
- _last = 0
- for i, item in enumerate(post_lines):
- if item['line']:
- _last = i
- post_lines = post_lines[:_last + 1]
- ret['post_lines'] = post_lines
- return ret
- 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
- # TODO: clean up & reimplement -- specifically for syntax errors
- def format_exception_only(etype, value):
- """Format the exception part of a traceback.
- The arguments are the exception type and value such as given by
- sys.last_type and sys.last_value. The return value is a list of
- strings, each ending in a newline.
- Normally, the list contains a single string; however, for
- SyntaxError exceptions, it contains several lines that (when
- printed) display detailed information about where the syntax
- error occurred.
- The message indicating which exception occurred is always the last
- string in the list.
- """
- # Gracefully handle (the way Python 2.4 and earlier did) the case of
- # being called with (None, None).
- if etype is None:
- return [_format_final_exc_line(etype, value)]
- stype = etype.__name__
- smod = etype.__module__
- if smod not in ("__main__", "builtins", "exceptions"):
- stype = smod + '.' + stype
- if not issubclass(etype, SyntaxError):
- return [_format_final_exc_line(stype, value)]
- # It was a syntax error; show exactly where the problem was found.
- lines = []
- filename = value.filename or "<string>"
- lineno = str(value.lineno) or '?'
- lines.append(' File "%s", line %s\n' % (filename, lineno))
- badline = value.text
- offset = value.offset
- if badline is not None:
- lines.append(' %s\n' % badline.strip())
- if offset is not None:
- caretspace = badline.rstrip('\n')[:offset].lstrip()
- # non-space whitespace (likes tabs) must be kept for alignment
- caretspace = ((c.isspace() and c or ' ') for c in caretspace)
- # only three spaces to account for offset1 == pos 0
- lines.append(' %s^\n' % ''.join(caretspace))
- msg = value.msg or "<no detail available>"
- lines.append("%s: %s\n" % (stype, msg))
- return lines
- # TODO: use asciify, improved if necessary
- def _some_str(value):
- try:
- return str(value)
- except Exception:
- pass
- try:
- value = text(value)
- return value.encode("ascii", "backslashreplace")
- except Exception:
- pass
- return '<unprintable %s object>' % type(value).__name__
- def _format_final_exc_line(etype, value):
- valuestr = _some_str(value)
- if value is None or not valuestr:
- line = "%s\n" % etype
- else:
- line = "%s: %s\n" % (etype, valuestr)
- return line
- def print_exception(etype, value, tb, limit=None, file=None):
- """Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
- This differs from print_tb() in the following ways: (1) if
- traceback is not None, it prints a header "Traceback (most recent
- call last):"; (2) it prints the exception type and value after the
- stack trace; (3) if type is SyntaxError and value has the
- appropriate format, it prints the line where the syntax error
- occurred with a caret on the next line indicating the approximate
- position of the error.
- """
- if file is None:
- file = sys.stderr
- if tb:
- tbi = TracebackInfo.from_traceback(tb, limit)
- print(str(tbi), end='', file=file)
- for line in format_exception_only(etype, value):
- print(line, end='', file=file)
- 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
- _frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)'
- r', in (?P<funcname>.+)$')
- _se_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)')
- # TODO: ParsedException generator over large bodies of text
- class ParsedException(object):
- """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
- self.exc_msg = exc_msg
- self.frames = list(frames or [])
- @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': list(self.frames)}
- def __repr__(self):
- cn = self.__class__.__name__
- return ('%s(%r, %r, frames=%r)'
- % (cn, self.exc_type, self.exc_msg, self.frames))
- def to_string(self):
- """Formats the exception and its traceback into the standard format,
- as returned by the traceback module.
- ``ParsedException.from_string(text).to_string()`` should yield
- ``text``.
- """
- lines = [u'Traceback (most recent call last):']
- for frame in self.frames:
- lines.append(u' File "%s", line %s, in %s' % (frame['filepath'],
- frame['lineno'],
- frame['funcname']))
- source_line = frame.get('source_line')
- if source_line:
- lines.append(u' %s' % (source_line,))
- if self.exc_msg:
- lines.append(u'%s: %s' % (self.exc_type, self.exc_msg))
- else:
- lines.append(u'%s' % (self.exc_type,))
- return u'\n'.join(lines)
- @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, text):
- tb_str = tb_str.decode('utf-8')
- tb_lines = tb_str.lstrip().splitlines()
- # First off, handle some ignored exceptions. These can be the
- # result of exceptions raised by __del__ during garbage
- # collection
- while tb_lines:
- cl = tb_lines[-1]
- if cl.startswith('Exception ') and cl.endswith('ignored'):
- 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:
- raise ValueError('unrecognized traceback string format')
- frames = []
- line_no = start_line
- while True:
- frame_line = tb_lines[line_no].strip()
- frame_match = frame_re.match(frame_line)
- if frame_match:
- frame_dict = frame_match.groupdict()
- try:
- next_line = tb_lines[line_no + 1]
- except IndexError:
- # We read what we could
- next_line = ''
- next_line_stripped = next_line.strip()
- if (
- frame_re.match(next_line_stripped) or
- # The exception message will not be indented
- # This check is to avoid overrunning on eval-like
- # tracebacks where the last frame doesn't have source
- # code in the traceback
- not next_line.startswith(' ')
- ):
- frame_dict['source_line'] = ''
- else:
- frame_dict['source_line'] = next_line_stripped
- line_no += 1
- else:
- break
- line_no += 1
- frames.append(frame_dict)
- try:
- exc_line = '\n'.join(tb_lines[line_no:])
- exc_type, _, exc_msg = exc_line.partition(': ')
- except Exception:
- exc_type, exc_msg = '', ''
- return cls(exc_type, exc_msg, frames)
- ParsedTB = ParsedException # legacy alias
|