diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 78599e704b1317..ce25ab730624ba 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -287,14 +287,33 @@ Object Protocol .. versionadded:: 3.3 +.. c:function:: int PyObject_GetDictPtr(PyObject *obj, PyObject ***dict_ptr) + + Get a pointer to :py:attr:`~object.__dict__` of the object *obj*. + + * If there is a ``__dict__``, set *\*dict_ptr* and return ``1``. + * If there is no ``__dict__``, set *\*dict_ptr* to ``NULL`` without setting + an exception and return ``0``. + * On error, set an exception and return ``-1``. + + This function may need to allocate memory for the dictionary, so it may be + more efficient to call :c:func:`PyObject_GetAttr` when accessing an + attribute on the object. + + .. versionadded:: next + + .. seealso:: + :c:func:`PyObject_GenericGetDict` and :c:func:`PyObject_GenericSetDict` + functions. + + .. c:function:: PyObject** _PyObject_GetDictPtr(PyObject *obj) - Return a pointer to :py:attr:`~object.__dict__` of the object *obj*. - If there is no ``__dict__``, return ``NULL`` without setting an exception. + Similar to :c:func:`PyObject_GetDictPtr`, but ignore errors silently (return + ``NULL`` without setting an exception). - This function may need to allocate memory for the - dictionary, so it may be more efficient to call :c:func:`PyObject_GetAttr` - when accessing an attribute on the object. + .. deprecated:: 3.15 + Use :c:func:`PyObject_GetDictPtr` instead. .. c:function:: PyObject* PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 4b176d6c8e6034..5bbe1d6e7eb489 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -852,6 +852,10 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:type:`PyObject_GetDictPtr` function to get a pointer to + :py:attr:`~object.__dict__` of an object. + (Contributed by Victor Stinner in :gh:`139852`.) + Porting to Python 3.15 ---------------------- @@ -912,6 +916,10 @@ Deprecated C APIs since 3.15 and will be removed in 3.17. (Contributed by Nikita Sobolev in :gh:`136355`.) +* Deprecate private :c:func:`_PyObject_GetDictPtr` function: + use public :c:func:`PyObject_GetDictPtr` instead. + (Contributed by Victor Stinner in :gh:`139852`.) + .. Add C API deprecations above alphabetically, not here at the end. diff --git a/Include/cpython/object.h b/Include/cpython/object.h index e2f87524c218b6..16e3b2a9526771 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -299,6 +299,7 @@ PyAPI_FUNC(void) _PyObject_Dump(PyObject *); PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); +PyAPI_FUNC(int) PyObject_GetDictPtr(PyObject *obj, PyObject ***dict_ptr); PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *); PyAPI_FUNC(void) PyObject_CallFinalizer(PyObject *); PyAPI_FUNC(int) PyObject_CallFinalizerFromDealloc(PyObject *); diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index d4056727d07fbf..998713bee22e2b 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -247,5 +247,24 @@ def func(x): func(object()) + def test_object_getdictptr(self): + object_getdictptr = _testcapi.object_getdictptr + + class MyClass: + pass + obj = MyClass() + obj.attr = 123 + + dict1 = object_getdictptr(obj) + dict2 = obj.__dict__ + self.assertIs(dict1, dict2) + + class NoDict: + __slots__ = () + obj = NoDict() + + self.assertEqual(object_getdictptr(obj), AttributeError) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst b/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst new file mode 100644 index 00000000000000..3b96a1ed099795 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst @@ -0,0 +1,2 @@ +Add :c:type:`PyObject_GetDictPtr` function to get a pointer to +:py:attr:`~object.__dict__` of an object. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst new file mode 100644 index 00000000000000..a4081c139476df --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst @@ -0,0 +1,2 @@ +Deprecate private :c:func:`_PyObject_GetDictPtr` function: use public +:c:func:`PyObject_GetDictPtr` instead. diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 798ef97c495aeb..1700cea0605098 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -485,6 +485,28 @@ is_uniquely_referenced(PyObject *self, PyObject *op) } +static PyObject * +object_getdictptr(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + + PyObject **dict_ptr = UNINITIALIZED_PTR; + switch (PyObject_GetDictPtr(obj, &dict_ptr)) { + case -1: + assert(dict_ptr == NULL); + return NULL; + case 0: + assert(dict_ptr == NULL); + return Py_NewRef(PyExc_AttributeError); + case 1: + return Py_NewRef(*dict_ptr); + default: + Py_FatalError("PyObject_GetDictPtr() returned invalid code"); + Py_UNREACHABLE(); + } +} + + static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS}, @@ -511,6 +533,7 @@ 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_getdictptr", object_getdictptr, METH_O}, {NULL}, }; diff --git a/Objects/object.c b/Objects/object.c index 0540112d7d2acf..897a4c1644e5ac 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1540,6 +1540,27 @@ _PyObject_ComputedDictPointer(PyObject *obj) return (PyObject **) ((char *)obj + dictoffset); } +int +PyObject_GetDictPtr(PyObject *obj, PyObject ***dict_ptr) +{ + if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) { + *dict_ptr = _PyObject_ComputedDictPointer(obj); + return (*dict_ptr != NULL); + } + + PyDictObject *dict = _PyObject_GetManagedDict(obj); + if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) { + dict = _PyObject_MaterializeManagedDict(obj); + if (dict == NULL) { + *dict_ptr = NULL; + return -1; + } + } + *dict_ptr = (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict; + assert(*dict_ptr != NULL); + return 1; +} + /* Helper to get a pointer to an object's __dict__ slot, if any. * Creates the dict from inline attributes if necessary. * Does not set an exception. @@ -1550,18 +1571,12 @@ _PyObject_ComputedDictPointer(PyObject *obj) PyObject ** _PyObject_GetDictPtr(PyObject *obj) { - if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) { - return _PyObject_ComputedDictPointer(obj); - } - PyDictObject *dict = _PyObject_GetManagedDict(obj); - if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) { - dict = _PyObject_MaterializeManagedDict(obj); - if (dict == NULL) { - PyErr_Clear(); - return NULL; - } + PyObject **dict_ptr; + if (PyObject_GetDictPtr(obj, &dict_ptr) < 0) { + PyErr_Clear(); + dict_ptr = NULL; } - return (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict; + return dict_ptr; } PyObject *