Commit deee39f4 authored by Jason Madden's avatar Jason Madden

Update tblib to 2.0

parent 85a6d079
...@@ -11,3 +11,5 @@ Related changes include: ...@@ -11,3 +11,5 @@ Related changes include:
- ``setup.py`` no longer includes the ``setup_requires`` keyword. - ``setup.py`` no longer includes the ``setup_requires`` keyword.
Installation with a tool that understands ``pyproject.toml`` is Installation with a tool that understands ``pyproject.toml`` is
recommended. recommended.
- The bundled tblib has been updated to version 2.0.
- On Python 3.12b3, dumping tracebacks of greenlets is not available.
...@@ -23,105 +23,15 @@ ...@@ -23,105 +23,15 @@
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#### ####
# cpython.py
"""
Taken verbatim from Jinja2.
https://github.com/mitsuhiko/jinja2/blob/master/jinja2/debug.py#L267
"""
# pylint:disable=consider-using-dict-comprehension,bad-dunder-name
#import platform # XXX: gevent cannot import platform at the top level; interferes with monkey patching
import sys
def _init_ugly_crap():
"""This function implements a few ugly things so that we can patch the
traceback objects. The function returned allows resetting `tb_next` on
any python traceback object. Do not attempt to use this on non cpython
interpreters
"""
import ctypes
from types import TracebackType
# figure out side of _Py_ssize_t
if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
_Py_ssize_t = ctypes.c_int64
else:
_Py_ssize_t = ctypes.c_int
# regular python
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
# python with trace
if hasattr(sys, 'getobjects'):
class _PyObject(ctypes.Structure):
pass
_PyObject._fields_ = [
('_ob_next', ctypes.POINTER(_PyObject)),
('_ob_prev', ctypes.POINTER(_PyObject)),
('ob_refcnt', _Py_ssize_t),
('ob_type', ctypes.POINTER(_PyObject))
]
class _Traceback(_PyObject):
pass
_Traceback._fields_ = [
('tb_next', ctypes.POINTER(_Traceback)),
('tb_frame', ctypes.POINTER(_PyObject)),
('tb_lasti', ctypes.c_int),
('tb_lineno', ctypes.c_int)
]
def tb_set_next(tb, next):
"""Set the tb_next attribute of a traceback object."""
if not (isinstance(tb, TracebackType) and (next is None or isinstance(next, TracebackType))):
raise TypeError('tb_set_next arguments must be traceback objects')
obj = _Traceback.from_address(id(tb))
if tb.tb_next is not None:
old = _Traceback.from_address(id(tb.tb_next))
old.ob_refcnt -= 1
if next is None:
obj.tb_next = ctypes.POINTER(_Traceback)()
else:
next = _Traceback.from_address(id(next))
next.ob_refcnt += 1
obj.tb_next = ctypes.pointer(next)
return tb_set_next
tb_set_next = None
#try:
# if platform.python_implementation() == 'CPython':
# tb_set_next = _init_ugly_crap()
#except Exception as exc:
# sys.stderr.write("Failed to initialize cpython support: {!r}".format(exc))
#del _init_ugly_crap
# __init__.py # __init__.py
import re import re
import sys
from types import CodeType from types import CodeType
from types import FrameType
from types import TracebackType
try: __version__ = '2.0.0'
from __pypy__ import tproxy __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code'
except ImportError:
tproxy = None
__version__ = '1.3.0'
__all__ = ('Traceback',)
PY3 = sys.version_info[0] >= 3
FRAME_RE = re.compile(r'^\s*File "(?P<co_filename>.+)", line (?P<tb_lineno>\d+)(, in (?P<co_name>.+))?$') FRAME_RE = re.compile(r'^\s*File "(?P<co_filename>.+)", line (?P<tb_lineno>\d+)(, in (?P<co_name>.+))?$')
...@@ -132,7 +42,7 @@ class _AttrDict(dict): ...@@ -132,7 +42,7 @@ class _AttrDict(dict):
try: try:
return self[name] return self[name]
except KeyError: except KeyError:
raise AttributeError(name) raise AttributeError(name) from None
# noinspection PyPep8Naming # noinspection PyPep8Naming
...@@ -144,60 +54,49 @@ class TracebackParseError(Exception): ...@@ -144,60 +54,49 @@ class TracebackParseError(Exception):
pass pass
class Code(object): class Code:
"""
Class that replicates just enough of the builtin Code object to enable serialization and traceback rendering.
"""
co_code = None
def __init__(self, code): def __init__(self, code):
self.co_filename = code.co_filename self.co_filename = code.co_filename
self.co_name = code.co_name self.co_name = code.co_name
self.co_argcount = 0 self.co_argcount = 0
self.co_kwonlyargcount = 0 self.co_kwonlyargcount = 0
self.co_varnames = () self.co_varnames = ()
# gevent: copy more attributes self.co_nlocals = 0
self.co_nlocals = code.co_nlocals self.co_stacksize = 0
self.co_stacksize = code.co_stacksize self.co_flags = 64
self.co_flags = code.co_flags self.co_firstlineno = 0
self.co_firstlineno = code.co_firstlineno
def __reduce__(self):
return Code, (_AttrDict(self.__dict__),)
# noinspection SpellCheckingInspection
def __tproxy__(self, operation, *args, **kwargs):
if operation in ('__getattribute__', '__getattr__'):
return getattr(self, args[0])
else:
return getattr(self, operation)(*args, **kwargs)
class Frame(object): class Frame:
"""
Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering.
"""
def __init__(self, frame): def __init__(self, frame):
self.f_locals = {} self.f_locals = {}
self.f_globals = dict([ self.f_globals = {k: v for k, v in frame.f_globals.items() if k in ('__file__', '__name__')}
(k, v)
for k, v in frame.f_globals.items()
if k in ("__file__", "__name__")
])
self.f_code = Code(frame.f_code) self.f_code = Code(frame.f_code)
self.f_lineno = frame.f_lineno self.f_lineno = frame.f_lineno
def clear(self): def clear(self):
# For compatibility with PyPy 3.5; """
# clear was added to frame in Python 3.4 For compatibility with PyPy 3.5;
# and is called by traceback.clear_frames(), which clear() was added to frame in Python 3.4
# in turn is called by unittest.TestCase.assertRaises and is called by traceback.clear_frames(), which
pass in turn is called by unittest.TestCase.assertRaises
"""
# noinspection SpellCheckingInspection
def __tproxy__(self, operation, *args, **kwargs):
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 Traceback:
"""
Class that wraps builtin Traceback objects.
"""
tb_next = None tb_next = None
...@@ -219,48 +118,46 @@ class Traceback(object): ...@@ -219,48 +118,46 @@ class Traceback(object):
tb = tb.tb_next tb = tb.tb_next
def as_traceback(self): def as_traceback(self):
if tproxy: """
return tproxy(TracebackType, self.__tproxy__) Convert to a builtin Traceback object that is usable for raising or rendering a stacktrace.
if not tb_set_next: """
raise RuntimeError("Cannot re-create traceback !")
current = self current = self
top_tb = None top_tb = None
tb = None tb = None
while current: while current:
f_code = current.tb_frame.f_code 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') code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec')
if hasattr(code, "replace"): if hasattr(code, 'replace'):
# Python 3.8 and newer # Python 3.8 and newer
code = code.replace(co_argcount=0, code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=())
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: else:
code = CodeType( code = CodeType(
0, 0,
code.co_nlocals, code.co_stacksize, code.co_flags, code.co_kwonlyargcount,
code.co_code, code.co_consts, code.co_names, code.co_varnames, code.co_nlocals,
f_code.co_filename.encode(), f_code.co_name.encode(), code.co_stacksize,
code.co_firstlineno, code.co_lnotab, (), () 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,
(),
(),
) )
# noinspection PyBroadException # noinspection PyBroadException
try: try:
exec(code, dict(current.tb_frame.f_globals), {}) exec(code, dict(current.tb_frame.f_globals), {}) # noqa: S102
except: except Exception:
next_tb = sys.exc_info()[2].tb_next next_tb = sys.exc_info()[2].tb_next
if top_tb is None: if top_tb is None:
top_tb = next_tb top_tb = next_tb
if tb is not None: if tb is not None:
tb_set_next(tb, next_tb) tb.tb_next = next_tb
tb = next_tb tb = next_tb
del next_tb del next_tb
...@@ -270,23 +167,14 @@ class Traceback(object): ...@@ -270,23 +167,14 @@ class Traceback(object):
finally: finally:
del top_tb del top_tb
del tb del tb
to_traceback = as_traceback
to_traceback = as_traceback
# noinspection SpellCheckingInspection
def __tproxy__(self, operation, *args, **kwargs):
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): def as_dict(self):
"""Convert a Traceback into a dictionary representation""" """
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: if self.tb_next is None:
tb_next = None tb_next = None
else: else:
...@@ -306,10 +194,14 @@ class Traceback(object): ...@@ -306,10 +194,14 @@ class Traceback(object):
'tb_lineno': self.tb_lineno, 'tb_lineno': self.tb_lineno,
'tb_next': tb_next, 'tb_next': tb_next,
} }
to_dict = as_dict to_dict = as_dict
@classmethod @classmethod
def from_dict(cls, dct): def from_dict(cls, dct):
"""
Creates an instance from a dictionary with the same structure as ``.as_dict()`` returns.
"""
if dct['tb_next']: if dct['tb_next']:
tb_next = cls.from_dict(dct['tb_next']) tb_next = cls.from_dict(dct['tb_next'])
else: else:
...@@ -333,6 +225,10 @@ class Traceback(object): ...@@ -333,6 +225,10 @@ class Traceback(object):
@classmethod @classmethod
def from_string(cls, string, strict=True): 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 = [] frames = []
header = strict header = strict
...@@ -368,9 +264,22 @@ class Traceback(object): ...@@ -368,9 +264,22 @@ class Traceback(object):
) )
return cls(previous) return cls(previous)
else: else:
raise TracebackParseError("Could not find any frames in %r." % string) raise TracebackParseError('Could not find any frames in %r.' % string)
# pickling_support.py # pickling_support.py
# gevent: Trying the dict support, so maybe we don't even need this
# at all.
import sys
from types import TracebackType
#from . import Frame # gevent
#from . import Traceback # gevent
# gevent: defer
# if sys.version_info.major >= 3:
# import copyreg
# else:
# import copy_reg as copyreg
def unpickle_traceback(tb_frame, tb_lineno, tb_next): def unpickle_traceback(tb_frame, tb_lineno, tb_next):
...@@ -385,92 +294,83 @@ def pickle_traceback(tb): ...@@ -385,92 +294,83 @@ def pickle_traceback(tb):
return unpickle_traceback, (Frame(tb.tb_frame), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next)) return unpickle_traceback, (Frame(tb.tb_frame), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next))
def install(): def unpickle_exception(func, args, cause, tb):
try: inst = func(*args)
import copy_reg inst.__cause__ = cause
except ImportError: inst.__traceback__ = tb
import copyreg as copy_reg return inst
copy_reg.pickle(TracebackType, pickle_traceback)
def pickle_exception(obj):
# Added by gevent # All exceptions, unlike generic Python objects, define __reduce_ex__
# __reduce_ex__(4) should be no different from __reduce_ex__(3).
# We have to defer the initialization, and especially the import of platform, # __reduce_ex__(5) could bring benefits in the unlikely case the exception
# until runtime. If we're monkey patched, we need to be sure to use # directly contains buffers, but PickleBuffer objects will cause a crash when
# the original __import__ to avoid switching through the hub due to # running on protocol=4, and there's no clean way to figure out the current
# import locks on Python 2. See also builtins.py for details. # protocol from here. Note that any object returned by __reduce_ex__(3) will
# still be pickled with protocol 5 if pickle.dump() is running with it.
rv = obj.__reduce_ex__(3)
def _unlocked_imports(f): if isinstance(rv, str):
def g(a): raise TypeError('str __reduce__ output is not supported')
if sys is None: # pragma: no cover assert isinstance(rv, tuple)
# interpreter shutdown on Py2 assert len(rv) >= 2
return
return (unpickle_exception, rv[:2] + (obj.__cause__, obj.__traceback__)) + rv[2:]
gb = None
if 'gevent.builtins' in sys.modules:
gb = sys.modules['gevent.builtins'] def _get_subclasses(cls):
gb._unlock_imports() # Depth-first traversal of all direct and indirect subclasses of cls
try: to_visit = [cls]
return f(a) while to_visit:
finally: this = to_visit.pop()
if gb is not None: yield this
gb._lock_imports() to_visit += list(this.__subclasses__())
g.__name__ = f.__name__
g.__module__ = f.__module__
return g def install(*exc_classes_or_instances):
import copyreg
copyreg.pickle(TracebackType, pickle_traceback)
def _import_dump_load():
global dumps if sys.version_info.major < 3:
global loads # Dummy decorator?
try: if len(exc_classes_or_instances) == 1:
import cPickle as pickle exc = exc_classes_or_instances[0]
except ImportError: if isinstance(exc, type) and issubclass(exc, BaseException):
import pickle return exc
dumps = pickle.dumps
loads = pickle.loads
dumps = loads = None
_installed = False
def _init():
global _installed
global tb_set_next
if _installed:
return return
_installed = True if not exc_classes_or_instances:
import platform for exception_cls in _get_subclasses(BaseException):
try: copyreg.pickle(exception_cls, pickle_exception)
if platform.python_implementation() == 'CPython': return
tb_set_next = _init_ugly_crap()
except Exception as exc:
sys.stderr.write("Failed to initialize cpython support: {!r}".format(exc))
try:
from __pypy__ import tproxy
except ImportError:
tproxy = None
if not tb_set_next and not tproxy:
raise ImportError("Cannot use tblib. Runtime not supported.")
_import_dump_load()
install()
for exc in exc_classes_or_instances:
if isinstance(exc, BaseException):
while exc is not None:
copyreg.pickle(type(exc), pickle_exception)
exc = exc.__cause__
elif isinstance(exc, type) and issubclass(exc, BaseException):
copyreg.pickle(exc, pickle_exception)
# Allow using @install as a decorator for Exception classes
if len(exc_classes_or_instances) == 1:
return exc
else:
raise TypeError('Expected subclasses or instances of BaseException, got %s' % (type(exc)))
@_unlocked_imports # gevent API
_installed = False
def dump_traceback(tb): def dump_traceback(tb):
# Both _init and dump/load have to be unlocked, because from pickle import dumps
# copy_reg and pickle can do imports to resolve class names; those if tb is None:
# class names are in this module and greenlet safe though return dumps(None)
_init() tb = Traceback(tb)
return dumps(tb) return dumps(tb.to_dict())
@_unlocked_imports
def load_traceback(s): def load_traceback(s):
_init() from pickle import loads
return loads(s) as_dict = loads(s)
if as_dict is None:
return None
tb = Traceback.from_dict(as_dict)
return tb.as_traceback()
...@@ -374,16 +374,42 @@ class GreenletTree(object): ...@@ -374,16 +374,42 @@ class GreenletTree(object):
@staticmethod @staticmethod
def __render_tb(tree, label, frame, limit): def __render_tb(tree, label, frame, limit):
tree.child_data(label) tree.child_data(label)
# XXX: Issues with tblib? # XXX: Issues with tblib? Seen with tblib 1.3 and 2.0.
# 3.12b3 is crashing walking the stack on macOS; # More likely, it's something wrong in greenlet and the way it's
# on Linux CI, it is failing with a nice attribute error # keeping track of the frames?
#
# In a test like this:
#
# g = gevent.spawn(util.print_run_info, file=io)
# g.join() (test__util.py, line 53)
#
# 3.12b3 is crashing walking the stack on macOS:
# It's a simple segfault on line 340, ``f = f.f_back``.
# I have confirmed that the object is a real frame object.
#
# It seems unlikely to be a greenlet thing though, because the frame we're
# crashing on is the root frame:
#
# <frame at 0x.., file '/gevent/tests/test__util.py', line 53, code root>
#
# Interestingly, we see the test case dump the stack of the greenlet (successfully),
# then dump the stack of the main thread (successfully) --- this ends in line 53 --,
# and then get _another_ frame for line 53, and this is where it crashes.
# The difference? The successful dump does not list it as a root frame,
# where the failed one does.
#
#
# on Linux CI (not sure what frame), it is failing with a nice attribute error
# (which watches where the macOS is failing, inside a call to # (which watches where the macOS is failing, inside a call to
# Py_GetAttr): # Py_GetAttr):
# #
# File "/opt/hostedtoolcache/Python/3.12.0-beta.3/x64/lib/python3.12/traceback.py", line 339, in walk_stack # File "//python3.12/traceback.py", line 339, in walk_stack
# yield f, f.f_lineno # yield f, f.f_lineno
# AttributeError: 'dict' object has no attribute 'f_lineno' # AttributeError: 'dict' object has no attribute 'f_lineno'
#
# A workaround on macOS is to not dump the root frame, but that only fixes
# test__util. test__threadpool:test_greenlet_class crashes similarly, but
# not 100% of the time.
if sys.version_info != (3, 12, 0, 'beta', 3): if sys.version_info != (3, 12, 0, 'beta', 3):
tb = ''.join(traceback.format_stack(frame, limit)) tb = ''.join(traceback.format_stack(frame, limit))
else: else:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment