If you really need to fool another library—especially one written in C and using the non-public API—there are two potential ways to get a real traceback object. I haven't gotten either one to work reliably. Also, both are CPython-specific, require not just using the C API layer but using undocumented types and functions that could change at any moment, and offer the potential for new and exciting opportunities to segfault your interpreter. But if you want to try, they may be useful for a start.
The PyTraceBack
type is not part of the public API. But (except for being defined in the Python directory instead of the Object directory) it's built as a C API type, just not documented. So, if you look at traceback.h
and traceback.c
for your Python version, you'll see that… well, there's no PyTraceBack_New
, but there is a PyTraceBack_Here
that constructs a new traceback and swaps it into the current exception info. I'm not sure it's valid to call this unless there's a current exception, and if there is a current exception you might be screwing it up by mutating it like this, but with a bit of trial&crash or reading the code, hopefully you can get this to work:
import ctypes
import sys
ctypes.pythonapi.PyTraceBack_Here.argtypes = (ctypes.py_object,)
ctypes.pythonapi.PyTraceBack_Here.restype = ctypes.c_int
def _fake_tb():
try:
1/0
except:
frame = sys._getframe(2)
if ctypes.pythonapi.PyTraceBack_Here(frame):
raise RuntimeError('Oops, probably hosed the interpreter')
raise
def get_tb():
try:
_fake_tb()
except ZeroDivisionError as e:
return e.__traceback__
As a fun alternative, we can try to mutate a traceback object on the fly. To get a traceback object, just raise and catch an exception:
try: 1/0
except exception as e: tb = e.__traceback__ # or sys.exc_info()[2]
The only problem is that it's pointing at your stack frame, not your caller's, right? If tracebacks were mutable, you could fix that easily:
tb.tb_lasti, tb.tb_lineno = tb.tb_frame.f_lasti, tb.tb_frame.f_lineno
tb.tb_frame = tb.tb_frame.f_back
And there's no methods for setting these things, either. Notice that it doesn't have a setattro
, and its getattro
works by building a __dict__
on the fly, so obviously the only way we're getting at this stuff is through the underlying struct. Which you should really build with ctypes.Structure
, but as a quick hack:
p8 = ctypes.cast(id(tb), ctypes.POINTER(ctypes.c_ulong))
p4 = ctypes.cast(id(tb), ctypes.POINTER(ctypes.c_uint))
Now, for a normal 64-bit build of CPython, p8[:2]
/ p4[:4]
are the normal object header, and after that come the traceback-specific fields, so p8[3]
is the tb_frame
, and p4[8]
and p4[9]
are the tb_lasti
and tb_lineno
, respectively. So:
p4[8], p4[9] = tb.tb_frame.f_lasti, tb.tb_frame.f_lineno
But the next part is a bit harder, because tb_frame
isn't actually a PyObject *
, it's just a raw struct _frame *
, so off you go to frameobject.h
, where you see that it really is a PyFrameObject *
so you can just use the same trick again. Just remember to _ctypes.Py_INCREF
the frame's next frame and Py_DECREF
the frame itself after doing reassigning p8[3]
to point at pf8[3]
, or as soon as you try to print the traceback you'll segfault and lose all the work you'd done writing this up. :)
traceback.extract_stack
and/orinspect.stack
isn't atraceback
object, but it's not just a list of lines, either. Is one of those sufficient? – Rushingisinstance(t, types.TracebackType)
? If so… well, I wouldn't be surprised iftraceback
is one of the handful of types that won't allow itself to be used as a base, but I'd at least check first. If not, what does it mean? – Rushing