import re import sys from types import CodeType from types import FrameType from types import TracebackType try: from __pypy__ import tproxy except ImportError: tproxy = None try: from .cpython import tb_set_next except ImportError: tb_set_next = None if not tb_set_next and not tproxy: raise ImportError("Cannot use tblib. Runtime not supported.") __version__ = '1.7.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' PY3 = sys.version_info[0] == 3 FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') class _AttrDict(dict): __slots__ = () def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError(name) # noinspection PyPep8Naming class __traceback_maker(Exception): pass class TracebackParseError(Exception): pass class Code(object): """ Class that replicates just enough of the builtin Code object to enable serialization and traceback rendering. """ co_code = None def __init__(self, code): self.co_filename = code.co_filename self.co_name = code.co_name self.co_argcount = 0 self.co_kwonlyargcount = 0 self.co_varnames = () self.co_nlocals = 0 self.co_stacksize = 0 self.co_flags = 64 self.co_firstlineno = 0 # noinspection SpellCheckingInspection def __tproxy__(self, operation, *args, **kwargs): """ Necessary for PyPy's tproxy. """ if operation in ('__getattribute__', '__getattr__'): return getattr(self, args[0]) else: return getattr(self, operation)(*args, **kwargs) class Frame(object): """ Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering. """ def __init__(self, frame): self.f_locals = {} self.f_globals = { k: v for k, v in frame.f_globals.items() if k in ("__file__", "__name__") } self.f_code = Code(frame.f_code) self.f_lineno = frame.f_lineno def clear(self): """ For compatibility with PyPy 3.5; clear() was added to frame in Python 3.4 and is called by traceback.clear_frames(), which in turn is called by unittest.TestCase.assertRaises """ # noinspection SpellCheckingInspection def __tproxy__(self, operation, *args, **kwargs): """ Necessary for PyPy's tproxy. """ if operation in ('__getattribute__', '__getattr__'): if args[0] == 'f_code': return tproxy(CodeType, self.f_code.__tproxy__) else: return getattr(self, args[0]) else: return getattr(self, operation)(*args, **kwargs) class Traceback(object): """ Class that wraps builtin Traceback objects. """ tb_next = None def __init__(self, tb): self.tb_frame = Frame(tb.tb_frame) # noinspection SpellCheckingInspection self.tb_lineno = int(tb.tb_lineno) # Build in place to avoid exceeding the recursion limit tb = tb.tb_next prev_traceback = self cls = type(self) while tb is not None: traceback = object.__new__(cls) traceback.tb_frame = Frame(tb.tb_frame) traceback.tb_lineno = int(tb.tb_lineno) prev_traceback.tb_next = traceback prev_traceback = traceback tb = tb.tb_next def as_traceback(self): """ Convert to a builtin Traceback object that is usable for raising or rendering a stacktrace. """ if tproxy: return tproxy(TracebackType, self.__tproxy__) if not tb_set_next: raise RuntimeError("Unsupported Python interpreter!") current = self top_tb = None tb = None while current: f_code = current.tb_frame.f_code code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec') if hasattr(code, "replace"): # Python 3.8 and newer code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=()) elif PY3: code = CodeType( 0, code.co_kwonlyargcount, code.co_nlocals, code.co_stacksize, code.co_flags, code.co_code, code.co_consts, code.co_names, code.co_varnames, f_code.co_filename, f_code.co_name, code.co_firstlineno, code.co_lnotab, (), () ) else: code = CodeType( 0, code.co_nlocals, code.co_stacksize, code.co_flags, code.co_code, code.co_consts, code.co_names, code.co_varnames, f_code.co_filename.encode(), f_code.co_name.encode(), code.co_firstlineno, code.co_lnotab, (), () ) # noinspection PyBroadException try: exec(code, dict(current.tb_frame.f_globals), {}) except Exception: next_tb = sys.exc_info()[2].tb_next if top_tb is None: top_tb = next_tb if tb is not None: tb_set_next(tb, next_tb) tb = next_tb del next_tb current = current.tb_next try: return top_tb finally: del top_tb del tb to_traceback = as_traceback # noinspection SpellCheckingInspection def __tproxy__(self, operation, *args, **kwargs): """ Necessary for PyPy's tproxy. """ if operation in ('__getattribute__', '__getattr__'): if args[0] == 'tb_next': return self.tb_next and self.tb_next.as_traceback() elif args[0] == 'tb_frame': return tproxy(FrameType, self.tb_frame.__tproxy__) else: return getattr(self, args[0]) else: return getattr(self, operation)(*args, **kwargs) def as_dict(self): """ Converts to a dictionary representation. You can serialize the result to JSON as it only has builtin objects like dicts, lists, ints or strings. """ if self.tb_next is None: tb_next = None else: tb_next = self.tb_next.to_dict() code = { 'co_filename': self.tb_frame.f_code.co_filename, 'co_name': self.tb_frame.f_code.co_name, } frame = { 'f_globals': self.tb_frame.f_globals, 'f_code': code, 'f_lineno': self.tb_frame.f_lineno, } return { 'tb_frame': frame, 'tb_lineno': self.tb_lineno, 'tb_next': tb_next, } to_dict = as_dict @classmethod def from_dict(cls, dct): """ Creates an instance from a dictionary with the same structure as ``.as_dict()`` returns. """ if dct['tb_next']: tb_next = cls.from_dict(dct['tb_next']) else: tb_next = None code = _AttrDict( co_filename=dct['tb_frame']['f_code']['co_filename'], co_name=dct['tb_frame']['f_code']['co_name'], ) frame = _AttrDict( f_globals=dct['tb_frame']['f_globals'], f_code=code, f_lineno=dct['tb_frame']['f_lineno'], ) tb = _AttrDict( tb_frame=frame, tb_lineno=dct['tb_lineno'], tb_next=tb_next, ) return cls(tb) @classmethod def from_string(cls, string, strict=True): """ Creates an instance by parsing a stacktrace. Strict means that parsing stops when lines are not indented by at least two spaces anymore. """ frames = [] header = strict for line in string.splitlines(): line = line.rstrip() if header: if line == 'Traceback (most recent call last):': header = False continue frame_match = FRAME_RE.match(line) if frame_match: frames.append(frame_match.groupdict()) elif line.startswith(' '): pass elif strict: break # traceback ended if frames: previous = None for frame in reversed(frames): previous = _AttrDict( frame, tb_frame=_AttrDict( frame, f_globals=_AttrDict( __file__=frame['co_filename'], __name__='?', ), f_code=_AttrDict(frame), f_lineno=int(frame['tb_lineno']), ), tb_next=previous, ) return cls(previous) else: raise TracebackParseError("Could not find any frames in %r." % string)