Skip to content

Commit e46a8af

Browse files
njsmithncoghlan
authored andcommitted
bpo-30579: Allow TracebackType creation and tb_next mutation from Python (GH-4793)
Third party projects may wish to hide their own internal machinery in order to present more comprehensible tracebacks to end users (e.g. Jinja2 and Trio both do this). Previously such projects have had to rely on ctypes to do so: https://github.com/pallets/jinja/blob/fe3dadacdf4cf411d0a5b6bbd4d5234697a28af2/jinja2/debug.py#L345 https://github.com/python-trio/trio/blob/1e86b1aee8c0c759f6f239ae53a05d0d3963c629/trio/_core/_multierror.py#L296 This provides a Python level API for creating and modifying real Traceback objects, allowing tracebacks to be edited at runtime. Patch by Nathaniel Smith.
1 parent d327ae6 commit e46a8af

File tree

4 files changed

+226
-26
lines changed

4 files changed

+226
-26
lines changed

Lib/test/test_raise.py

+66
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,72 @@ def test_accepts_traceback(self):
228228
self.fail("No exception raised")
229229

230230

231+
class TestTracebackType(unittest.TestCase):
232+
233+
def raiser(self):
234+
raise ValueError
235+
236+
def test_attrs(self):
237+
try:
238+
self.raiser()
239+
except Exception as exc:
240+
tb = exc.__traceback__
241+
242+
self.assertIsInstance(tb.tb_next, types.TracebackType)
243+
self.assertIs(tb.tb_frame, sys._getframe())
244+
self.assertIsInstance(tb.tb_lasti, int)
245+
self.assertIsInstance(tb.tb_lineno, int)
246+
247+
self.assertIs(tb.tb_next.tb_next, None)
248+
249+
# Invalid assignments
250+
with self.assertRaises(TypeError):
251+
del tb.tb_next
252+
253+
with self.assertRaises(TypeError):
254+
tb.tb_next = "asdf"
255+
256+
# Loops
257+
with self.assertRaises(ValueError):
258+
tb.tb_next = tb
259+
260+
with self.assertRaises(ValueError):
261+
tb.tb_next.tb_next = tb
262+
263+
# Valid assignments
264+
tb.tb_next = None
265+
self.assertIs(tb.tb_next, None)
266+
267+
new_tb = get_tb()
268+
tb.tb_next = new_tb
269+
self.assertIs(tb.tb_next, new_tb)
270+
271+
def test_constructor(self):
272+
other_tb = get_tb()
273+
frame = sys._getframe()
274+
275+
tb = types.TracebackType(other_tb, frame, 1, 2)
276+
self.assertEqual(tb.tb_next, other_tb)
277+
self.assertEqual(tb.tb_frame, frame)
278+
self.assertEqual(tb.tb_lasti, 1)
279+
self.assertEqual(tb.tb_lineno, 2)
280+
281+
tb = types.TracebackType(None, frame, 1, 2)
282+
self.assertEqual(tb.tb_next, None)
283+
284+
with self.assertRaises(TypeError):
285+
types.TracebackType("no", frame, 1, 2)
286+
287+
with self.assertRaises(TypeError):
288+
types.TracebackType(other_tb, "no", 1, 2)
289+
290+
with self.assertRaises(TypeError):
291+
types.TracebackType(other_tb, frame, "no", 2)
292+
293+
with self.assertRaises(TypeError):
294+
types.TracebackType(other_tb, frame, 1, "nuh-uh")
295+
296+
231297
class TestContext(unittest.TestCase):
232298
def test_instance_context_instance_raise(self):
233299
context = IndexError()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement TracebackType.__new__ to allow Python-level creation of
2+
traceback objects, and make TracebackType.tb_next mutable.

Python/clinic/traceback.c.h

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*[clinic input]
2+
preserve
3+
[clinic start generated code]*/
4+
5+
PyDoc_STRVAR(tb_new__doc__,
6+
"TracebackType(tb_next, tb_frame, tb_lasti, tb_lineno)\n"
7+
"--\n"
8+
"\n"
9+
"Create a new traceback object.");
10+
11+
static PyObject *
12+
tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame,
13+
int tb_lasti, int tb_lineno);
14+
15+
static PyObject *
16+
tb_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
17+
{
18+
PyObject *return_value = NULL;
19+
static const char * const _keywords[] = {"tb_next", "tb_frame", "tb_lasti", "tb_lineno", NULL};
20+
static _PyArg_Parser _parser = {"OO!ii:TracebackType", _keywords, 0};
21+
PyObject *tb_next;
22+
PyFrameObject *tb_frame;
23+
int tb_lasti;
24+
int tb_lineno;
25+
26+
if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &_parser,
27+
&tb_next, &PyFrame_Type, &tb_frame, &tb_lasti, &tb_lineno)) {
28+
goto exit;
29+
}
30+
return_value = tb_new_impl(type, tb_next, tb_frame, tb_lasti, tb_lineno);
31+
32+
exit:
33+
return return_value;
34+
}
35+
/*[clinic end generated code: output=0133130d7d19556f input=a9049054013a1b77]*/

Python/traceback.c

+123-26
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,138 @@ _Py_IDENTIFIER(close);
2727
_Py_IDENTIFIER(open);
2828
_Py_IDENTIFIER(path);
2929

30+
/*[clinic input]
31+
class TracebackType "PyTracebackObject *" "&PyTraceback_Type"
32+
[clinic start generated code]*/
33+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=928fa06c10151120]*/
34+
35+
#include "clinic/traceback.c.h"
36+
37+
static PyObject *
38+
tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti,
39+
int lineno)
40+
{
41+
PyTracebackObject *tb;
42+
if ((next != NULL && !PyTraceBack_Check(next)) ||
43+
frame == NULL || !PyFrame_Check(frame)) {
44+
PyErr_BadInternalCall();
45+
return NULL;
46+
}
47+
tb = PyObject_GC_New(PyTracebackObject, &PyTraceBack_Type);
48+
if (tb != NULL) {
49+
Py_XINCREF(next);
50+
tb->tb_next = next;
51+
Py_XINCREF(frame);
52+
tb->tb_frame = frame;
53+
tb->tb_lasti = lasti;
54+
tb->tb_lineno = lineno;
55+
PyObject_GC_Track(tb);
56+
}
57+
return (PyObject *)tb;
58+
}
59+
60+
/*[clinic input]
61+
@classmethod
62+
TracebackType.__new__ as tb_new
63+
64+
tb_next: object
65+
tb_frame: object(type='PyFrameObject *', subclass_of='&PyFrame_Type')
66+
tb_lasti: int
67+
tb_lineno: int
68+
69+
Create a new traceback object.
70+
[clinic start generated code]*/
71+
72+
static PyObject *
73+
tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame,
74+
int tb_lasti, int tb_lineno)
75+
/*[clinic end generated code: output=fa077debd72d861a input=01cbe8ec8783fca7]*/
76+
{
77+
if (tb_next == Py_None) {
78+
tb_next = NULL;
79+
} else if (!PyTraceBack_Check(tb_next)) {
80+
return PyErr_Format(PyExc_TypeError,
81+
"expected traceback object or None, got '%s'",
82+
Py_TYPE(tb_next)->tp_name);
83+
}
84+
85+
return tb_create_raw((PyTracebackObject *)tb_next, tb_frame, tb_lasti,
86+
tb_lineno);
87+
}
88+
3089
static PyObject *
3190
tb_dir(PyTracebackObject *self)
3291
{
3392
return Py_BuildValue("[ssss]", "tb_frame", "tb_next",
3493
"tb_lasti", "tb_lineno");
3594
}
3695

96+
static PyObject *
97+
tb_next_get(PyTracebackObject *self, void *Py_UNUSED(_))
98+
{
99+
PyObject* ret = (PyObject*)self->tb_next;
100+
if (!ret) {
101+
ret = Py_None;
102+
}
103+
Py_INCREF(ret);
104+
return ret;
105+
}
106+
107+
static int
108+
tb_next_set(PyTracebackObject *self, PyObject *new_next, void *Py_UNUSED(_))
109+
{
110+
if (!new_next) {
111+
PyErr_Format(PyExc_TypeError, "can't delete tb_next attribute");
112+
return -1;
113+
}
114+
115+
/* We accept None or a traceback object, and map None -> NULL (inverse of
116+
tb_next_get) */
117+
if (new_next == Py_None) {
118+
new_next = NULL;
119+
} else if (!PyTraceBack_Check(new_next)) {
120+
PyErr_Format(PyExc_TypeError,
121+
"expected traceback object, got '%s'",
122+
Py_TYPE(new_next)->tp_name);
123+
return -1;
124+
}
125+
126+
/* Check for loops */
127+
PyTracebackObject *cursor = (PyTracebackObject *)new_next;
128+
while (cursor) {
129+
if (cursor == self) {
130+
PyErr_Format(PyExc_ValueError, "traceback loop detected");
131+
return -1;
132+
}
133+
cursor = cursor->tb_next;
134+
}
135+
136+
PyObject *old_next = (PyObject*)self->tb_next;
137+
Py_XINCREF(new_next);
138+
self->tb_next = (PyTracebackObject *)new_next;
139+
Py_XDECREF(old_next);
140+
141+
return 0;
142+
}
143+
144+
37145
static PyMethodDef tb_methods[] = {
38146
{"__dir__", (PyCFunction)tb_dir, METH_NOARGS},
39147
{NULL, NULL, 0, NULL},
40148
};
41149

42150
static PyMemberDef tb_memberlist[] = {
43-
{"tb_next", T_OBJECT, OFF(tb_next), READONLY},
44151
{"tb_frame", T_OBJECT, OFF(tb_frame), READONLY},
45152
{"tb_lasti", T_INT, OFF(tb_lasti), READONLY},
46153
{"tb_lineno", T_INT, OFF(tb_lineno), READONLY},
47154
{NULL} /* Sentinel */
48155
};
49156

157+
static PyGetSetDef tb_getsetters[] = {
158+
{"tb_next", (getter)tb_next_get, (setter)tb_next_set, NULL, NULL},
159+
{NULL} /* Sentinel */
160+
};
161+
50162
static void
51163
tb_dealloc(PyTracebackObject *tb)
52164
{
@@ -94,7 +206,7 @@ PyTypeObject PyTraceBack_Type = {
94206
0, /* tp_setattro */
95207
0, /* tp_as_buffer */
96208
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
97-
0, /* tp_doc */
209+
tb_new__doc__, /* tp_doc */
98210
(traverseproc)tb_traverse, /* tp_traverse */
99211
(inquiry)tb_clear, /* tp_clear */
100212
0, /* tp_richcompare */
@@ -103,39 +215,24 @@ PyTypeObject PyTraceBack_Type = {
103215
0, /* tp_iternext */
104216
tb_methods, /* tp_methods */
105217
tb_memberlist, /* tp_members */
106-
0, /* tp_getset */
218+
tb_getsetters, /* tp_getset */
107219
0, /* tp_base */
108220
0, /* tp_dict */
221+
0, /* tp_descr_get */
222+
0, /* tp_descr_set */
223+
0, /* tp_dictoffset */
224+
0, /* tp_init */
225+
0, /* tp_alloc */
226+
tb_new, /* tp_new */
109227
};
110228

111-
static PyTracebackObject *
112-
newtracebackobject(PyTracebackObject *next, PyFrameObject *frame)
113-
{
114-
PyTracebackObject *tb;
115-
if ((next != NULL && !PyTraceBack_Check(next)) ||
116-
frame == NULL || !PyFrame_Check(frame)) {
117-
PyErr_BadInternalCall();
118-
return NULL;
119-
}
120-
tb = PyObject_GC_New(PyTracebackObject, &PyTraceBack_Type);
121-
if (tb != NULL) {
122-
Py_XINCREF(next);
123-
tb->tb_next = next;
124-
Py_XINCREF(frame);
125-
tb->tb_frame = frame;
126-
tb->tb_lasti = frame->f_lasti;
127-
tb->tb_lineno = PyFrame_GetLineNumber(frame);
128-
PyObject_GC_Track(tb);
129-
}
130-
return tb;
131-
}
132-
133229
int
134230
PyTraceBack_Here(PyFrameObject *frame)
135231
{
136232
PyObject *exc, *val, *tb, *newtb;
137233
PyErr_Fetch(&exc, &val, &tb);
138-
newtb = (PyObject *)newtracebackobject((PyTracebackObject *)tb, frame);
234+
newtb = tb_create_raw((PyTracebackObject *)tb, frame, frame->f_lasti,
235+
PyFrame_GetLineNumber(frame));
139236
if (newtb == NULL) {
140237
_PyErr_ChainExceptions(exc, val, tb);
141238
return -1;

0 commit comments

Comments
 (0)