How to create a traceback object
There's no documented way to create traceback objects.
None of the functions in the traceback
module create them. You can of course access the type as types.TracebackType
, but if you call its constructor you just get a TypeError: cannot create 'traceback' instances
.
The reason for this is that tracebacks contain references to internals that you can't actually access or generate from within Python.
However, you can access stack frames, and everything else you'd need to simulate a traceback is trivial. You can even write a class that has tb_frame
, tb_lasti
, tb_lineno
, and tb_next
attributes (using the info you can get from traceback.extract_stack
and one of the inspect
functions), which will look exactly like a traceback to any pure-Python code.
So there's a good chance that whatever you really want to do is doable, even though what you're asking for is not.
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. :)
"In order to better support dynamic creation of stack traces, types.TracebackType can now be instantiated from Python code, and the tb_next attribute on tracebacks is now writable."
There is an explanation(in python 3.7) for the same in here(python 3.7) https://docs.python.org/3/library/types.html#types.TracebackType
Since Python 3.7 you can create traceback objects dynamically from Python.
To create traceback identical to one created by raise:
raise Exception()
use this:
import sys
import types
def exception_with_traceback(message):
tb = None
depth = 0
while True:
try:
frame = sys._getframe(depth)
depth += 1
except ValueError as exc:
break
tb = types.TracebackType(tb, frame, frame.f_lasti, frame.f_lineno)
return Exception(message).with_traceback(tb)
Relevant documentation is here:
- https://docs.python.org/3/library/types.html#types.TracebackType
- https://docs.python.org/3/reference/datamodel.html#traceback-objects
- https://docs.python.org/3/library/sys.html#sys._getframe