Mercurial > repos > shellac > sam_consensus_v3
diff env/lib/python3.9/site-packages/boltons/tbutils.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lib/python3.9/site-packages/boltons/tbutils.py Mon Mar 22 18:12:50 2021 +0000 @@ -0,0 +1,796 @@ +# -*- coding: utf-8 -*- +"""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