Skip to content

Commit 7211538

Browse files
committed
bpo-30579: Allow TracebackType creation and tb_next mutation from Python
This is admittedly an obscure use case, but currently there are projects like Jinja2 and Trio that are forced to work around this with horrible ctypes hacks: https://github.com/pallets/jinja/blob/fe3dadacdf4cf411d0a5b6bbd4d5234697a28af2/jinja2/debug.py#L345 https://github.com/python-trio/trio/blob/1e86b1aee8c0c759f6f239ae53a05d0d3963c629/trio/_core/_multierror.py#L296
1 parent 19d0d54 commit 7211538

File tree

4 files changed

+226
-26
lines changed

4 files changed

+226
-26
lines changed

Lib/test/test_raise.py

Lines changed: 66 additions & 0 deletions
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()
Lines changed: 2 additions & 0 deletions
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

Lines changed: 35 additions & 0 deletions
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

Lines changed: 123 additions & 26 deletions
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)