Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-102594: PyErr_SetObject adds note to exception raised on normalization error #102675

Merged
merged 13 commits into from
Mar 16, 2023
Merged
4 changes: 4 additions & 0 deletions Include/cpython/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCause(

/* In exceptions.c */

PyAPI_FUNC(PyObject*) _PyException_AddNote(
PyBaseExceptionObject *exc,
PyObject *note);

/* Helper that attempts to replace the current exception with one of the
* same type but with a prefix added to the exception text. The resulting
* exception description looks like:
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_capi/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,25 @@ class Broken(Exception, metaclass=Meta):
with self.assertRaises(ZeroDivisionError) as e:
_testcapi.exc_set_object(Broken, Broken())

def test_set_object_and_fetch(self):
class Broken(Exception):
def __init__(self, *arg):
raise ValueError("Broken __init__")

exc = _testcapi.exc_set_object_fetch(Broken, ('abcd'))
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
self.assertIsInstance(exc, ValueError)
self.assertEqual(exc.__notes__[0],
"Normalization failed: type=Broken args='abcd'")

class BadArg:
def __repr__(self):
raise TypeError('Broken arg type')

exc = _testcapi.exc_set_object_fetch(Broken, (BadArg()))
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
self.assertIsInstance(exc, ValueError)
self.assertEqual(exc.__notes__[0],
'Normalization failed: type=Broken args=<unknown>')


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add note to exception raised in ``PyErr_SetObject`` when normalization fails.
21 changes: 21 additions & 0 deletions Modules/_testcapi/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ exc_set_object(PyObject *self, PyObject *args)
return NULL;
}

static PyObject *
exc_set_object_fetch(PyObject *self, PyObject *args)
{
PyObject *exc;
PyObject *obj;
PyObject *type;
PyObject *value;
PyObject *tb;

if (!PyArg_ParseTuple(args, "OO:exc_set_object", &exc, &obj)) {
return NULL;
}

PyErr_SetObject(exc, obj);
PyErr_Fetch(&type, &value, &tb);
Py_XDECREF(type);
Py_XDECREF(tb);
return value;
}

static PyObject *
raise_exception(PyObject *self, PyObject *args)
{
Expand Down Expand Up @@ -262,6 +282,7 @@ static PyMethodDef test_methods[] = {
{"make_exception_with_doc", _PyCFunction_CAST(make_exception_with_doc),
METH_VARARGS | METH_KEYWORDS},
{"exc_set_object", exc_set_object, METH_VARARGS},
{"exc_set_object_fetch", exc_set_object_fetch, METH_VARARGS},
{"raise_exception", raise_exception, METH_VARARGS},
{"raise_memoryerror", raise_memoryerror, METH_NOARGS},
{"set_exc_info", test_set_exc_info, METH_VARARGS},
Expand Down
6 changes: 6 additions & 0 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -3749,6 +3749,12 @@ _PyExc_Fini(PyInterpreterState *interp)
_PyExc_FiniTypes(interp);
}

PyObject *
_PyException_AddNote(PyBaseExceptionObject *exc, PyObject *note)
{
return BaseException_add_note((PyObject *)exc, note);
}

/* Helper to do the equivalent of "raise X from Y" in C, but always using
* the current exception rather than passing one in.
*
Expand Down
32 changes: 32 additions & 0 deletions Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ _PyErr_GetTopmostException(PyThreadState *tstate)
return exc_info;
}

static PyObject *
get_normalization_failure_note(PyThreadState *tstate, PyObject *exception, PyObject *value)
{
PyObject *args = PyObject_Repr(value);
if (args == NULL) {
_PyErr_Clear(tstate);
args = PyUnicode_FromFormat("<unknown>");
}
PyObject *note;
const char *tpname = ((PyTypeObject*)exception)->tp_name;
if (args == NULL) {
_PyErr_Clear(tstate);
note = PyUnicode_FromFormat("Normalization failed: type=%s", tpname);
}
else {
note = PyUnicode_FromFormat("Normalization failed: type=%s args=%S",
tpname, args);
Py_DECREF(args);
}
return note;
}

void
_PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
{
Expand Down Expand Up @@ -169,6 +191,16 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
fixed_value = _PyErr_CreateException(exception, value);
Py_XDECREF(value);
if (fixed_value == NULL) {
PyObject *exc = _PyErr_GetRaisedException(tstate);
assert(PyExceptionInstance_Check(exc));

PyObject *note = get_normalization_failure_note(tstate, exception, value);
if (note != NULL) {
PyObject *res = _PyException_AddNote((PyBaseExceptionObject*)exc, note);
Py_DECREF(note);
Py_XDECREF(res);
}
_PyErr_SetRaisedException(tstate, exc);
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
return;
}

Expand Down