Skip to content

Commit 51d693c

Browse files
authored
gh-102594: PyErr_SetObject adds note to exception raised on normalization error (#102675)
1 parent 2dc9463 commit 51d693c

File tree

6 files changed

+96
-5
lines changed

6 files changed

+96
-5
lines changed

Include/cpython/pyerrors.h

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCause(
112112

113113
/* In exceptions.c */
114114

115+
PyAPI_FUNC(int) _PyException_AddNote(
116+
PyObject *exc,
117+
PyObject *note);
118+
115119
/* Helper that attempts to replace the current exception with one of the
116120
* same type but with a prefix added to the exception text. The resulting
117121
* exception description looks like:

Lib/test/test_capi/test_exceptions.py

+20
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,25 @@ class Broken(Exception, metaclass=Meta):
169169
with self.assertRaises(ZeroDivisionError) as e:
170170
_testcapi.exc_set_object(Broken, Broken())
171171

172+
def test_set_object_and_fetch(self):
173+
class Broken(Exception):
174+
def __init__(self, *arg):
175+
raise ValueError("Broken __init__")
176+
177+
exc = _testcapi.exc_set_object_fetch(Broken, 'abcd')
178+
self.assertIsInstance(exc, ValueError)
179+
self.assertEqual(exc.__notes__[0],
180+
"Normalization failed: type=Broken args='abcd'")
181+
182+
class BadArg:
183+
def __repr__(self):
184+
raise TypeError('Broken arg type')
185+
186+
exc = _testcapi.exc_set_object_fetch(Broken, BadArg())
187+
self.assertIsInstance(exc, ValueError)
188+
self.assertEqual(exc.__notes__[0],
189+
'Normalization failed: type=Broken args=<unknown>')
190+
191+
172192
if __name__ == "__main__":
173193
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add note to exception raised in ``PyErr_SetObject`` when normalization fails.

Modules/_testcapi/exceptions.c

+21
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ exc_set_object(PyObject *self, PyObject *args)
9292
return NULL;
9393
}
9494

95+
static PyObject *
96+
exc_set_object_fetch(PyObject *self, PyObject *args)
97+
{
98+
PyObject *exc;
99+
PyObject *obj;
100+
PyObject *type;
101+
PyObject *value;
102+
PyObject *tb;
103+
104+
if (!PyArg_ParseTuple(args, "OO:exc_set_object", &exc, &obj)) {
105+
return NULL;
106+
}
107+
108+
PyErr_SetObject(exc, obj);
109+
PyErr_Fetch(&type, &value, &tb);
110+
Py_XDECREF(type);
111+
Py_XDECREF(tb);
112+
return value;
113+
}
114+
95115
static PyObject *
96116
raise_exception(PyObject *self, PyObject *args)
97117
{
@@ -262,6 +282,7 @@ static PyMethodDef test_methods[] = {
262282
{"make_exception_with_doc", _PyCFunction_CAST(make_exception_with_doc),
263283
METH_VARARGS | METH_KEYWORDS},
264284
{"exc_set_object", exc_set_object, METH_VARARGS},
285+
{"exc_set_object_fetch", exc_set_object_fetch, METH_VARARGS},
265286
{"raise_exception", raise_exception, METH_VARARGS},
266287
{"raise_memoryerror", raise_memoryerror, METH_NOARGS},
267288
{"set_exc_info", test_set_exc_info, METH_VARARGS},

Objects/exceptions.c

+15
Original file line numberDiff line numberDiff line change
@@ -3749,6 +3749,21 @@ _PyExc_Fini(PyInterpreterState *interp)
37493749
_PyExc_FiniTypes(interp);
37503750
}
37513751

3752+
int
3753+
_PyException_AddNote(PyObject *exc, PyObject *note)
3754+
{
3755+
if (!PyExceptionInstance_Check(exc)) {
3756+
PyErr_Format(PyExc_TypeError,
3757+
"exc must be an exception, not '%s'",
3758+
Py_TYPE(exc)->tp_name);
3759+
return -1;
3760+
}
3761+
PyObject *r = BaseException_add_note(exc, note);
3762+
int res = r == NULL ? -1 : 0;
3763+
Py_XDECREF(r);
3764+
return res;
3765+
}
3766+
37523767
/* Helper to do the equivalent of "raise X from Y" in C, but always using
37533768
* the current exception rather than passing one in.
37543769
*

Python/errors.c

+35-5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,28 @@ _PyErr_GetTopmostException(PyThreadState *tstate)
135135
return exc_info;
136136
}
137137

138+
static PyObject *
139+
get_normalization_failure_note(PyThreadState *tstate, PyObject *exception, PyObject *value)
140+
{
141+
PyObject *args = PyObject_Repr(value);
142+
if (args == NULL) {
143+
_PyErr_Clear(tstate);
144+
args = PyUnicode_FromFormat("<unknown>");
145+
}
146+
PyObject *note;
147+
const char *tpname = ((PyTypeObject*)exception)->tp_name;
148+
if (args == NULL) {
149+
_PyErr_Clear(tstate);
150+
note = PyUnicode_FromFormat("Normalization failed: type=%s", tpname);
151+
}
152+
else {
153+
note = PyUnicode_FromFormat("Normalization failed: type=%s args=%S",
154+
tpname, args);
155+
Py_DECREF(args);
156+
}
157+
return note;
158+
}
159+
138160
void
139161
_PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
140162
{
@@ -160,19 +182,27 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
160182
Py_XINCREF(value);
161183
if (!is_subclass) {
162184
/* We must normalize the value right now */
163-
PyObject *fixed_value;
164185

165186
/* Issue #23571: functions must not be called with an
166187
exception set */
167188
_PyErr_Clear(tstate);
168189

169-
fixed_value = _PyErr_CreateException(exception, value);
170-
Py_XDECREF(value);
190+
PyObject *fixed_value = _PyErr_CreateException(exception, value);
171191
if (fixed_value == NULL) {
192+
PyObject *exc = _PyErr_GetRaisedException(tstate);
193+
assert(PyExceptionInstance_Check(exc));
194+
195+
PyObject *note = get_normalization_failure_note(tstate, exception, value);
196+
Py_XDECREF(value);
197+
if (note != NULL) {
198+
/* ignore errors in _PyException_AddNote - they will be overwritten below */
199+
_PyException_AddNote(exc, note);
200+
Py_DECREF(note);
201+
}
202+
_PyErr_SetRaisedException(tstate, exc);
172203
return;
173204
}
174-
175-
value = fixed_value;
205+
Py_XSETREF(value, fixed_value);
176206
}
177207

178208
exc_value = _PyErr_GetTopmostException(tstate)->exc_value;

0 commit comments

Comments
 (0)