From 145291db28ab5ea94808a3703a31575abb179dee Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 1 Jul 2025 16:19:53 +0200 Subject: [PATCH 01/10] gh-135075: Make PyObject_SetAttr() fail with NULL value and exception Make PyObject_SetAttr() and PyObject_SetAttrString() fail if called with NULL value and an exception set. --- Doc/c-api/object.rst | 4 ++ Lib/test/test_capi/test_object.py | 15 ++++++++ ...-07-01-16-22-39.gh-issue-135075.angu3J.rst | 2 + Modules/_testcapi/object.c | 37 ++++++++++++++++++ Objects/object.c | 38 ++++++++++++++----- 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-07-01-16-22-39.gh-issue-135075.angu3J.rst diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 0fd159f1eb87f8..a1de82e2df2837 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -197,6 +197,8 @@ Object Protocol in favour of using :c:func:`PyObject_DelAttr`, but there are currently no plans to remove it. + The function must not be called with ``NULL`` *v* and an an exception set. + .. c:function:: int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v) @@ -207,6 +209,8 @@ Object Protocol If *v* is ``NULL``, the attribute is deleted, but this feature is deprecated in favour of using :c:func:`PyObject_DelAttrString`. + The function must not be called with ``NULL`` *v* and an an exception set. + The number of different attribute names passed to this function should be kept small, usually by using a statically allocated string as *attr_name*. diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index d4056727d07fbf..276bbbb54dfc1d 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -247,5 +247,20 @@ def func(x): func(object()) + def test_object_setattr_null_exc(self): + class Obj: + pass + obj = Obj() + + obj.attr = 123 + with self.assertRaises(SystemError): + _testcapi.object_setattr_null_exc(obj, 'attr') + self.assertTrue(hasattr(obj, 'attr')) + + with self.assertRaises(SystemError): + _testcapi.object_setattrstring_null_exc(obj, 'attr') + self.assertTrue(hasattr(obj, 'attr')) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-07-01-16-22-39.gh-issue-135075.angu3J.rst b/Misc/NEWS.d/next/C_API/2025-07-01-16-22-39.gh-issue-135075.angu3J.rst new file mode 100644 index 00000000000000..88e0fa65f45dd0 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-07-01-16-22-39.gh-issue-135075.angu3J.rst @@ -0,0 +1,2 @@ +Make :c:func:`PyObject_SetAttr` and :c:func:`PyObject_SetAttrString` fail if +called with ``NULL`` value and an exception set. Patch by Victor Stinner. diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 798ef97c495aeb..be30c17e522052 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -485,6 +485,41 @@ is_uniquely_referenced(PyObject *self, PyObject *op) } +static PyObject * +object_setattr_null_exc(PyObject *self, PyObject *args) +{ + PyObject *obj, *name; + if (!PyArg_ParseTuple(args, "OO", &obj, &name)) { + return NULL; + } + + PyErr_SetString(PyExc_ValueError, "error"); + if (PyObject_SetAttr(obj, name, NULL) < 0) { + return NULL; + } + assert(PyErr_Occurred()); + return NULL; +} + + +static PyObject * +object_setattrstring_null_exc(PyObject *self, PyObject *args) +{ + PyObject *obj; + const char *name; + if (!PyArg_ParseTuple(args, "Os", &obj, &name)) { + return NULL; + } + + PyErr_SetString(PyExc_ValueError, "error"); + if (PyObject_SetAttrString(obj, name, NULL) < 0) { + return NULL; + } + assert(PyErr_Occurred()); + return NULL; +} + + static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS}, @@ -511,6 +546,8 @@ static PyMethodDef test_methods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, + {"object_setattr_null_exc", object_setattr_null_exc, METH_VARARGS}, + {"object_setattrstring_null_exc", object_setattrstring_null_exc, METH_VARARGS}, {NULL}, }; diff --git a/Objects/object.c b/Objects/object.c index 1223983753ac46..516732d8b98f6c 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1213,16 +1213,27 @@ PyObject_HasAttrString(PyObject *obj, const char *name) int PyObject_SetAttrString(PyObject *v, const char *name, PyObject *w) { - PyObject *s; - int res; + if (Py_TYPE(v)->tp_setattr != NULL) { + PyThreadState *tstate = _PyThreadState_GET(); + if (w == NULL && _PyErr_Occurred(tstate)) { + PyObject *exc = _PyErr_GetRaisedException(tstate); + _PyErr_SetString(tstate, PyExc_SystemError, + "PyObject_SetAttrString() must not be called with NULL value " + "and an exception set"); + _PyErr_ChainExceptions1Tstate(tstate, exc); + return -1; + } - if (Py_TYPE(v)->tp_setattr != NULL) return (*Py_TYPE(v)->tp_setattr)(v, (char*)name, w); - s = PyUnicode_InternFromString(name); - if (s == NULL) + } + + PyObject *s = PyUnicode_InternFromString(name); + if (s == NULL) { return -1; - res = PyObject_SetAttr(v, s, w); - Py_XDECREF(s); + } + + int res = PyObject_SetAttr(v, s, w); + Py_DECREF(s); return res; } @@ -1440,6 +1451,16 @@ PyObject_HasAttr(PyObject *obj, PyObject *name) int PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) { + PyThreadState *tstate = _PyThreadState_GET(); + if (value == NULL && _PyErr_Occurred(tstate)) { + PyObject *exc = _PyErr_GetRaisedException(tstate); + _PyErr_SetString(tstate, PyExc_SystemError, + "PyObject_SetAttr() must not be called with NULL value " + "and an exception set"); + _PyErr_ChainExceptions1Tstate(tstate, exc); + return -1; + } + PyTypeObject *tp = Py_TYPE(v); int err; @@ -1451,8 +1472,7 @@ PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) } Py_INCREF(name); - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyUnicode_InternMortal(interp, &name); + _PyUnicode_InternMortal(tstate->interp, &name); if (tp->tp_setattro != NULL) { err = (*tp->tp_setattro)(v, name, value); Py_DECREF(name); From 3b78e5b3e3a3b16a240409b542b121e29102b22f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 1 Jul 2025 16:38:46 +0200 Subject: [PATCH 02/10] Complete the doc --- Doc/c-api/object.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index a1de82e2df2837..e61da97132159e 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -198,6 +198,8 @@ Object Protocol plans to remove it. The function must not be called with ``NULL`` *v* and an an exception set. + This case can arise from forgetting ``NULL`` checks and would delete the + attribute. .. c:function:: int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v) @@ -210,6 +212,8 @@ Object Protocol deprecated in favour of using :c:func:`PyObject_DelAttrString`. The function must not be called with ``NULL`` *v* and an an exception set. + This case can arise from forgetting ``NULL`` checks and would delete the + attribute. The number of different attribute names passed to this function should be kept small, usually by using a statically allocated string From bb6777bcb093b5ba1607256592cad400bcb25da8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 1 Jul 2025 17:22:37 +0200 Subject: [PATCH 03/10] Move the check --- Objects/object.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index 516732d8b98f6c..3ed7d55593dffa 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1213,17 +1213,17 @@ PyObject_HasAttrString(PyObject *obj, const char *name) int PyObject_SetAttrString(PyObject *v, const char *name, PyObject *w) { - if (Py_TYPE(v)->tp_setattr != NULL) { - PyThreadState *tstate = _PyThreadState_GET(); - if (w == NULL && _PyErr_Occurred(tstate)) { - PyObject *exc = _PyErr_GetRaisedException(tstate); - _PyErr_SetString(tstate, PyExc_SystemError, - "PyObject_SetAttrString() must not be called with NULL value " - "and an exception set"); - _PyErr_ChainExceptions1Tstate(tstate, exc); - return -1; - } + PyThreadState *tstate = _PyThreadState_GET(); + if (w == NULL && _PyErr_Occurred(tstate)) { + PyObject *exc = _PyErr_GetRaisedException(tstate); + _PyErr_SetString(tstate, PyExc_SystemError, + "PyObject_SetAttrString() must not be called with NULL value " + "and an exception set"); + _PyErr_ChainExceptions1Tstate(tstate, exc); + return -1; + } + if (Py_TYPE(v)->tp_setattr != NULL) { return (*Py_TYPE(v)->tp_setattr)(v, (char*)name, w); } From 402967d3a521107cdd96556bbd2a5e8b1a9f5645 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 14:43:40 +0200 Subject: [PATCH 04/10] Move tests to _testcapi/abstract.c --- Modules/_testcapi/abstract.c | 37 ++++++++++++++++++++++++++++++++++++ Modules/_testcapi/object.c | 37 ------------------------------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Modules/_testcapi/abstract.c b/Modules/_testcapi/abstract.c index d4045afd515909..3c7fe1af40a940 100644 --- a/Modules/_testcapi/abstract.c +++ b/Modules/_testcapi/abstract.c @@ -178,6 +178,41 @@ sequence_fast_get_item(PyObject *self, PyObject *args) } +static PyObject * +object_setattr_null_exc(PyObject *self, PyObject *args) +{ + PyObject *obj, *name; + if (!PyArg_ParseTuple(args, "OO", &obj, &name)) { + return NULL; + } + + PyErr_SetString(PyExc_ValueError, "error"); + if (PyObject_SetAttr(obj, name, NULL) < 0) { + return NULL; + } + assert(PyErr_Occurred()); + return NULL; +} + + +static PyObject * +object_setattrstring_null_exc(PyObject *self, PyObject *args) +{ + PyObject *obj; + const char *name; + if (!PyArg_ParseTuple(args, "Os", &obj, &name)) { + return NULL; + } + + PyErr_SetString(PyExc_ValueError, "error"); + if (PyObject_SetAttrString(obj, name, NULL) < 0) { + return NULL; + } + assert(PyErr_Occurred()); + return NULL; +} + + static PyMethodDef test_methods[] = { {"object_getoptionalattr", object_getoptionalattr, METH_VARARGS}, {"object_getoptionalattrstring", object_getoptionalattrstring, METH_VARARGS}, @@ -191,6 +226,8 @@ static PyMethodDef test_methods[] = { {"sequence_fast_get_size", sequence_fast_get_size, METH_O}, {"sequence_fast_get_item", sequence_fast_get_item, METH_VARARGS}, + {"object_setattr_null_exc", object_setattr_null_exc, METH_VARARGS}, + {"object_setattrstring_null_exc", object_setattrstring_null_exc, METH_VARARGS}, {NULL}, }; diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index be30c17e522052..798ef97c495aeb 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -485,41 +485,6 @@ is_uniquely_referenced(PyObject *self, PyObject *op) } -static PyObject * -object_setattr_null_exc(PyObject *self, PyObject *args) -{ - PyObject *obj, *name; - if (!PyArg_ParseTuple(args, "OO", &obj, &name)) { - return NULL; - } - - PyErr_SetString(PyExc_ValueError, "error"); - if (PyObject_SetAttr(obj, name, NULL) < 0) { - return NULL; - } - assert(PyErr_Occurred()); - return NULL; -} - - -static PyObject * -object_setattrstring_null_exc(PyObject *self, PyObject *args) -{ - PyObject *obj; - const char *name; - if (!PyArg_ParseTuple(args, "Os", &obj, &name)) { - return NULL; - } - - PyErr_SetString(PyExc_ValueError, "error"); - if (PyObject_SetAttrString(obj, name, NULL) < 0) { - return NULL; - } - assert(PyErr_Occurred()); - return NULL; -} - - static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS}, @@ -546,8 +511,6 @@ static PyMethodDef test_methods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, - {"object_setattr_null_exc", object_setattr_null_exc, METH_VARARGS}, - {"object_setattrstring_null_exc", object_setattrstring_null_exc, METH_VARARGS}, {NULL}, }; From a8d21f6108d7e68539f4554053b12aec9f7a87d7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 14:50:48 +0200 Subject: [PATCH 05/10] Enhance tests --- Lib/test/test_capi/test_object.py | 19 ++++++++++++------- Modules/_testcapi/abstract.c | 12 ++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 276bbbb54dfc1d..028b176b58b066 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -251,15 +251,20 @@ def test_object_setattr_null_exc(self): class Obj: pass obj = Obj() - obj.attr = 123 - with self.assertRaises(SystemError): - _testcapi.object_setattr_null_exc(obj, 'attr') - self.assertTrue(hasattr(obj, 'attr')) - with self.assertRaises(SystemError): - _testcapi.object_setattrstring_null_exc(obj, 'attr') - self.assertTrue(hasattr(obj, 'attr')) + exc = ValueError("error") + with self.assertRaises(SystemError) as cm: + _testcapi.object_setattr_null_exc(obj, 'attr', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') + + with self.assertRaises(SystemError) as cm: + _testcapi.object_setattrstring_null_exc(obj, 'attr', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') if __name__ == "__main__": diff --git a/Modules/_testcapi/abstract.c b/Modules/_testcapi/abstract.c index 3c7fe1af40a940..6a3901b283f149 100644 --- a/Modules/_testcapi/abstract.c +++ b/Modules/_testcapi/abstract.c @@ -181,12 +181,12 @@ sequence_fast_get_item(PyObject *self, PyObject *args) static PyObject * object_setattr_null_exc(PyObject *self, PyObject *args) { - PyObject *obj, *name; - if (!PyArg_ParseTuple(args, "OO", &obj, &name)) { + PyObject *obj, *name, *exc; + if (!PyArg_ParseTuple(args, "OOO", &obj, &name, &exc)) { return NULL; } - PyErr_SetString(PyExc_ValueError, "error"); + PyErr_SetObject((PyObject*)Py_TYPE(exc), exc); if (PyObject_SetAttr(obj, name, NULL) < 0) { return NULL; } @@ -198,13 +198,13 @@ object_setattr_null_exc(PyObject *self, PyObject *args) static PyObject * object_setattrstring_null_exc(PyObject *self, PyObject *args) { - PyObject *obj; + PyObject *obj, *exc; const char *name; - if (!PyArg_ParseTuple(args, "Os", &obj, &name)) { + if (!PyArg_ParseTuple(args, "OsO", &obj, &name, &exc)) { return NULL; } - PyErr_SetString(PyExc_ValueError, "error"); + PyErr_SetObject((PyObject*)Py_TYPE(exc), exc); if (PyObject_SetAttrString(obj, name, NULL) < 0) { return NULL; } From 7cc691dfafebaba58659ce606d5d7b0f65630144 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 14:52:22 +0200 Subject: [PATCH 06/10] Add versionchanged --- Doc/c-api/object.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index e61da97132159e..4a185751cf3658 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -201,6 +201,9 @@ Object Protocol This case can arise from forgetting ``NULL`` checks and would delete the attribute. + .. versionchanged:: next + Raise an exception if called with ``NULL`` *v* and an an exception set. + .. c:function:: int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v) @@ -223,6 +226,9 @@ Object Protocol For more details, see :c:func:`PyUnicode_InternFromString`, which may be used internally to create a key object. + .. versionchanged:: next + Raise an exception if called with ``NULL`` *v* and an an exception set. + .. c:function:: int PyObject_GenericSetAttr(PyObject *o, PyObject *name, PyObject *value) Generic attribute setter and deleter function that is meant From 3cb999df99bda06cdb2ff66514fc15dbe15454b5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 14:58:15 +0200 Subject: [PATCH 07/10] Test undecodable name --- Lib/test/test_capi/test_object.py | 7 +++++++ Modules/_testcapi/abstract.c | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 028b176b58b066..9e61d0fa91ec43 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -266,6 +266,13 @@ class Obj: self.assertIsNone(cm.exception.__cause__) self.assertHasAttr(obj, 'attr') + with self.assertRaises(SystemError) as cm: + # undecodable name + _testcapi.object_setattrstring_null_exc(obj, b'\xff', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') + if __name__ == "__main__": unittest.main() diff --git a/Modules/_testcapi/abstract.c b/Modules/_testcapi/abstract.c index 6a3901b283f149..c1f769456accef 100644 --- a/Modules/_testcapi/abstract.c +++ b/Modules/_testcapi/abstract.c @@ -200,7 +200,8 @@ object_setattrstring_null_exc(PyObject *self, PyObject *args) { PyObject *obj, *exc; const char *name; - if (!PyArg_ParseTuple(args, "OsO", &obj, &name, &exc)) { + Py_ssize_t size; + if (!PyArg_ParseTuple(args, "Oz#O", &obj, &name, &size, &exc)) { return NULL; } From d8ac1ac2313396331f6ff4299bda634c199dac07 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 16:37:58 +0200 Subject: [PATCH 08/10] Move tests to test_capi/test_abstract.py --- Lib/test/test_capi/test_abstract.py | 26 ++++++++++++++++++++++++++ Lib/test/test_capi/test_object.py | 26 -------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_capi/test_abstract.py b/Lib/test/test_capi/test_abstract.py index 7d548ae87c0fa6..21cacb201b02b9 100644 --- a/Lib/test/test_capi/test_abstract.py +++ b/Lib/test/test_capi/test_abstract.py @@ -1077,6 +1077,32 @@ def test_iter_nextitem(self): with self.assertRaisesRegex(TypeError, regex): PyIter_NextItem(10) + def test_object_setattr_null_exc(self): + class Obj: + pass + obj = Obj() + obj.attr = 123 + + exc = ValueError("error") + with self.assertRaises(SystemError) as cm: + _testcapi.object_setattr_null_exc(obj, 'attr', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') + + with self.assertRaises(SystemError) as cm: + _testcapi.object_setattrstring_null_exc(obj, 'attr', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') + + with self.assertRaises(SystemError) as cm: + # undecodable name + _testcapi.object_setattrstring_null_exc(obj, b'\xff', exc) + self.assertIs(cm.exception.__context__, exc) + self.assertIsNone(cm.exception.__cause__) + self.assertHasAttr(obj, 'attr') + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 9e61d0fa91ec43..1be5c08bdda3d5 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -247,32 +247,6 @@ def func(x): func(object()) - def test_object_setattr_null_exc(self): - class Obj: - pass - obj = Obj() - obj.attr = 123 - - exc = ValueError("error") - with self.assertRaises(SystemError) as cm: - _testcapi.object_setattr_null_exc(obj, 'attr', exc) - self.assertIs(cm.exception.__context__, exc) - self.assertIsNone(cm.exception.__cause__) - self.assertHasAttr(obj, 'attr') - - with self.assertRaises(SystemError) as cm: - _testcapi.object_setattrstring_null_exc(obj, 'attr', exc) - self.assertIs(cm.exception.__context__, exc) - self.assertIsNone(cm.exception.__cause__) - self.assertHasAttr(obj, 'attr') - - with self.assertRaises(SystemError) as cm: - # undecodable name - _testcapi.object_setattrstring_null_exc(obj, b'\xff', exc) - self.assertIs(cm.exception.__context__, exc) - self.assertIsNone(cm.exception.__cause__) - self.assertHasAttr(obj, 'attr') - if __name__ == "__main__": unittest.main() From 4267f325efd80d646e2057765ad36199409dda1f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 16:39:08 +0200 Subject: [PATCH 09/10] Remove useless test --- Lib/test/test_capi/test_abstract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_capi/test_abstract.py b/Lib/test/test_capi/test_abstract.py index 21cacb201b02b9..3a2ed9f5db82f0 100644 --- a/Lib/test/test_capi/test_abstract.py +++ b/Lib/test/test_capi/test_abstract.py @@ -1101,7 +1101,6 @@ class Obj: _testcapi.object_setattrstring_null_exc(obj, b'\xff', exc) self.assertIs(cm.exception.__context__, exc) self.assertIsNone(cm.exception.__cause__) - self.assertHasAttr(obj, 'attr') if __name__ == "__main__": From a11c2ee703afaa8427ddf370ff4b429bdf7b2529 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 2 Jul 2025 16:40:52 +0200 Subject: [PATCH 10/10] Rephrase versionchanged --- Doc/c-api/object.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 4a185751cf3658..21fa1491b33d86 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -202,7 +202,7 @@ Object Protocol attribute. .. versionchanged:: next - Raise an exception if called with ``NULL`` *v* and an an exception set. + Must not be called with NULL value if an exception is set. .. c:function:: int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v) @@ -227,7 +227,8 @@ Object Protocol used internally to create a key object. .. versionchanged:: next - Raise an exception if called with ``NULL`` *v* and an an exception set. + Must not be called with NULL value if an exception is set. + .. c:function:: int PyObject_GenericSetAttr(PyObject *o, PyObject *name, PyObject *value)