diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 0fd159f1eb87f8..d77dd59ff4eb42 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -197,6 +197,10 @@ Object Protocol in favour of using :c:func:`PyObject_DelAttr`, but there are currently no plans to remove it. + .. deprecated:: next + Calling this function with ``NULL`` *v* and an exception set is now + deprecated. + .. c:function:: int PyObject_SetAttrString(PyObject *o, const char *attr_name, PyObject *v) @@ -215,6 +219,11 @@ Object Protocol For more details, see :c:func:`PyUnicode_InternFromString`, which may be used internally to create a key object. + .. deprecated:: next + Calling this function with ``NULL`` *v* and an exception set is now + deprecated. + + .. c:function:: int PyObject_GenericSetAttr(PyObject *o, PyObject *name, PyObject *value) Generic attribute setter and deleter function that is meant diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index daf3e8fb6c2c2b..8056b93668625f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -322,7 +322,9 @@ Porting to Python 3.15 Deprecated C APIs ----------------- -* TODO +* Calling :c:func:`PyObject_SetAttr` and :c:func:`PyObject_SetAttrString` with + ``NULL`` value and an exception set is now deprecated. + (Contributed by Victor Stinner in :gh:`135075`.) .. Add C API deprecations above alphabetically, not here at the end. diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index d4056727d07fbf..509e6227ef46e3 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -247,5 +247,23 @@ def func(x): func(object()) + def test_object_setattr_null_exc(self): + class Obj: + pass + obj = Obj() + + obj.attr = 123 + with self.assertWarns(DeprecationWarning): + with self.assertRaises(ValueError): + _testcapi.object_setattr_null_exc(obj, 'attr') + self.assertFalse(hasattr(obj, 'attr')) + + obj.attr = 456 + with self.assertWarns(DeprecationWarning): + with self.assertRaises(ValueError): + _testcapi.object_setattrstring_null_exc(obj, 'attr') + self.assertFalse(hasattr(obj, 'attr')) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-06-03-14-55-43.gh-issue-135075.fq1iqH.rst b/Misc/NEWS.d/next/C_API/2025-06-03-14-55-43.gh-issue-135075.fq1iqH.rst new file mode 100644 index 00000000000000..88295cde9cc6ec --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-06-03-14-55-43.gh-issue-135075.fq1iqH.rst @@ -0,0 +1,3 @@ +Deprecate calling :c:func:`PyObject_SetAttr` and +:c:func:`PyObject_SetAttrString` with ``NULL`` value and an exception set is +now deprecated. 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 9fe61ba7f1593a..4870d0dc5675fa 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1212,8 +1212,18 @@ PyObject_SetAttrString(PyObject *v, const char *name, PyObject *w) PyObject *s; int res; - if (Py_TYPE(v)->tp_setattr != NULL) + if (Py_TYPE(v)->tp_setattr != NULL) { + if (w == NULL && PyErr_Occurred()) { + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 0, + "calling PyObject_SetAttrString() with NULL value " + "and an exception set is deprecated; " + "use PyObject_DelAttrString() instead")) { + return -1; + } + } + return (*Py_TYPE(v)->tp_setattr)(v, (char*)name, w); + } s = PyUnicode_InternFromString(name); if (s == NULL) return -1; @@ -1437,7 +1447,7 @@ int PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) { PyTypeObject *tp = Py_TYPE(v); - int err; + int res; if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, @@ -1447,25 +1457,37 @@ PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) } Py_INCREF(name); - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyUnicode_InternMortal(interp, &name); + PyThreadState *tstate = _PyThreadState_GET(); + PyObject *exc = NULL; + if (value == NULL && _PyErr_Occurred(tstate)) { + exc = _PyErr_GetRaisedException(tstate); + res = PyErr_WarnFormat(PyExc_DeprecationWarning, 0, + "calling PyObject_SetAttr() with NULL value " + "and an exception set is deprecated; " + "use PyObject_DelAttr() instead"); + if (res) { + res = -1; + goto done; + } + } + + _PyUnicode_InternMortal(tstate->interp, &name); + if (tp->tp_setattro != NULL) { - err = (*tp->tp_setattro)(v, name, value); - Py_DECREF(name); - return err; + res = (*tp->tp_setattro)(v, name, value); + goto done; } + if (tp->tp_setattr != NULL) { const char *name_str = PyUnicode_AsUTF8(name); if (name_str == NULL) { - Py_DECREF(name); - return -1; + res = -1; + goto done; } - err = (*tp->tp_setattr)(v, (char *)name_str, value); - Py_DECREF(name); - return err; + res = (*tp->tp_setattr)(v, (char *)name_str, value); + goto done; } - Py_DECREF(name); - _PyObject_ASSERT(name, Py_REFCNT(name) >= 1); + if (tp->tp_getattr == NULL && tp->tp_getattro == NULL) PyErr_Format(PyExc_TypeError, "'%.100s' object has no attributes " @@ -1480,7 +1502,15 @@ PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) tp->tp_name, value==NULL ? "del" : "assign to", name); - return -1; + res = -1; + goto done; + +done: + if (exc) { + _PyErr_ChainExceptions1Tstate(tstate, exc); + } + Py_DECREF(name); + return res; } int