Skip to content

Commit 79906b5

Browse files
committed
gh-139852: Add PyObject_GetDict() function
1 parent 5776d0d commit 79906b5

File tree

8 files changed

+169
-11
lines changed

8 files changed

+169
-11
lines changed

Doc/c-api/object.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ Object Protocol
268268
A generic implementation for the getter of a ``__dict__`` descriptor. It
269269
creates the dictionary if necessary.
270270
271+
Raise an :exc:`AttributeError` if the object has no ``__dict__``.
272+
271273
This function may also be called to get the :py:attr:`~object.__dict__`
272274
of the object *o*. Pass ``NULL`` for *context* when calling it.
273275
Since this function may need to allocate memory for the
@@ -287,6 +289,27 @@ Object Protocol
287289
.. versionadded:: 3.3
288290
289291
292+
.. c:function:: int PyObject_GetDict(PyObject *obj, PyObject **dict)
293+
294+
Get a pointer to :py:attr:`~object.__dict__` of the object *obj*.
295+
296+
* If there is a ``__dict__``, set *\*dict* to a :term:`strong reference`
297+
to the dictionary and return ``1``.
298+
* If there is no ``__dict__``, set *\*dict* to ``NULL`` without setting
299+
an exception and return ``0``.
300+
* On error, set an exception and return ``-1``.
301+
302+
This function may need to allocate memory for the dictionary, so it may be
303+
more efficient to call :c:func:`PyObject_GetAttr` when accessing an
304+
attribute on the object.
305+
306+
.. versionadded:: next
307+
308+
.. seealso::
309+
:c:func:`PyObject_GenericGetDict` and :c:func:`PyObject_GenericSetDict`
310+
functions.
311+
312+
290313
.. c:function:: PyObject** _PyObject_GetDictPtr(PyObject *obj)
291314
292315
Return a pointer to :py:attr:`~object.__dict__` of the object *obj*.
@@ -296,6 +319,9 @@ Object Protocol
296319
dictionary, so it may be more efficient to call :c:func:`PyObject_GetAttr`
297320
when accessing an attribute on the object.
298321
322+
.. deprecated:: 3.15
323+
Use :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead.
324+
299325
300326
.. c:function:: PyObject* PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid)
301327

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,10 @@ New features
852852

853853
(Contributed by Victor Stinner in :gh:`129813`.)
854854

855+
* Add :c:type:`PyObject_GetDict` function to get the
856+
:py:attr:`~object.__dict__` of an object.
857+
(Contributed by Victor Stinner in :gh:`139852`.)
858+
855859
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
856860
(Contributed by Victor Stinner in :gh:`111489`.)
857861

@@ -915,6 +919,10 @@ Deprecated C APIs
915919
since 3.15 and will be removed in 3.17.
916920
(Contributed by Nikita Sobolev in :gh:`136355`.)
917921

922+
* Deprecate private :c:func:`_PyObject_GetDictPtr` function:
923+
use public :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead.
924+
(Contributed by Victor Stinner in :gh:`139852`.)
925+
918926

919927
.. Add C API deprecations above alphabetically, not here at the end.
920928

Include/cpython/object.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ PyAPI_FUNC(void) _PyObject_Dump(PyObject *);
299299

300300
PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);
301301

302-
PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *);
302+
PyAPI_FUNC(int) PyObject_GetDict(PyObject *obj, PyObject **dict);
303+
Py_DEPRECATED(3.15) PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *);
303304
PyAPI_FUNC(void) PyObject_CallFinalizer(PyObject *);
304305
PyAPI_FUNC(int) PyObject_CallFinalizerFromDealloc(PyObject *);
305306

Lib/test/test_capi/test_object.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,50 @@ def func(x):
247247

248248
func(object())
249249

250+
def test_object_getdict(self):
251+
# Test PyObject_GetDict()
252+
object_getdict = _testcapi.object_getdict
253+
254+
class MyClass:
255+
pass
256+
obj = MyClass()
257+
obj.attr = 123
258+
259+
dict1 = object_getdict(obj)
260+
dict2 = obj.__dict__
261+
self.assertIs(dict1, dict2)
262+
263+
class NoDict:
264+
__slots__ = ()
265+
obj = NoDict()
266+
267+
self.assertEqual(object_getdict(obj), AttributeError)
268+
269+
# CRASHES object_getdict(NULL)
270+
271+
def test_object_genericgetdict(self):
272+
# Test PyObject_GenericGetDict()
273+
object_genericgetdict = _testcapi.object_genericgetdict
274+
275+
class MyClass:
276+
pass
277+
obj = MyClass()
278+
obj.attr = 123
279+
280+
dict1 = object_genericgetdict(obj)
281+
dict2 = obj.__dict__
282+
self.assertIs(dict1, dict2)
283+
284+
class NoDict:
285+
__slots__ = ()
286+
obj = NoDict()
287+
288+
with self.assertRaisesRegex(AttributeError,
289+
"This object has no __dict__"):
290+
object_genericgetdict(obj)
291+
292+
# CRASHES object_genericgetdict(NULL)
293+
294+
250295
if __name__ == "__main__":
251296
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:type:`PyObject_GetDict` function to get the :py:attr:`~object.__dict__`
2+
of an object. Patch by Victor Stinner.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Deprecate private :c:func:`_PyObject_GetDictPtr` function: use public
2+
:c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead.

Modules/_testcapi/object.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,45 @@ is_uniquely_referenced(PyObject *self, PyObject *op)
485485
}
486486

487487

488+
static PyObject *
489+
object_getdict(PyObject *self, PyObject *obj)
490+
{
491+
NULLABLE(obj);
492+
493+
PyObject *dict = UNINITIALIZED_PTR;
494+
switch (PyObject_GetDict(obj, &dict)) {
495+
case -1:
496+
assert(dict == NULL);
497+
return NULL;
498+
case 0:
499+
assert(dict == NULL);
500+
return Py_NewRef(PyExc_AttributeError);
501+
case 1:
502+
return dict;
503+
default:
504+
Py_FatalError("PyObject_GetDict() returned invalid code");
505+
Py_UNREACHABLE();
506+
}
507+
}
508+
509+
510+
static PyObject *
511+
object_genericgetdict(PyObject *self, PyObject *obj)
512+
{
513+
NULLABLE(obj);
514+
515+
PyObject *dict = PyObject_GenericGetDict(obj, NULL);
516+
if (dict != NULL) {
517+
return dict;
518+
}
519+
520+
if (PyErr_Occurred()) {
521+
return NULL;
522+
}
523+
return Py_NewRef(PyExc_AttributeError);
524+
}
525+
526+
488527
static PyMethodDef test_methods[] = {
489528
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
490529
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
@@ -511,6 +550,8 @@ static PyMethodDef test_methods[] = {
511550
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
512551
{"clear_managed_dict", clear_managed_dict, METH_O, NULL},
513552
{"is_uniquely_referenced", is_uniquely_referenced, METH_O},
553+
{"object_getdict", object_getdict, METH_O},
554+
{"object_genericgetdict", object_genericgetdict, METH_O},
514555
{NULL},
515556
};
516557

Objects/object.c

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,28 @@ _PyObject_ComputedDictPointer(PyObject *obj)
15401540
return (PyObject **) ((char *)obj + dictoffset);
15411541
}
15421542

1543+
static int
1544+
object_getdictptr(PyObject *obj, PyObject ***dict_ptr)
1545+
{
1546+
if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
1547+
*dict_ptr = _PyObject_ComputedDictPointer(obj);
1548+
return (*dict_ptr != NULL);
1549+
}
1550+
1551+
PyDictObject *dict = _PyObject_GetManagedDict(obj);
1552+
if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
1553+
dict = _PyObject_MaterializeManagedDict(obj);
1554+
if (dict == NULL) {
1555+
*dict_ptr = NULL;
1556+
return -1;
1557+
}
1558+
}
1559+
*dict_ptr = (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict;
1560+
assert(*dict_ptr != NULL);
1561+
return 1;
1562+
}
1563+
1564+
15431565
/* Helper to get a pointer to an object's __dict__ slot, if any.
15441566
* Creates the dict from inline attributes if necessary.
15451567
* Does not set an exception.
@@ -1550,20 +1572,31 @@ _PyObject_ComputedDictPointer(PyObject *obj)
15501572
PyObject **
15511573
_PyObject_GetDictPtr(PyObject *obj)
15521574
{
1553-
if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) {
1554-
return _PyObject_ComputedDictPointer(obj);
1575+
PyObject **dict_ptr;
1576+
if (object_getdictptr(obj, &dict_ptr) < 0) {
1577+
PyErr_Clear();
15551578
}
1556-
PyDictObject *dict = _PyObject_GetManagedDict(obj);
1557-
if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
1558-
dict = _PyObject_MaterializeManagedDict(obj);
1559-
if (dict == NULL) {
1560-
PyErr_Clear();
1561-
return NULL;
1562-
}
1579+
return dict_ptr;
1580+
}
1581+
1582+
1583+
int
1584+
PyObject_GetDict(PyObject *obj, PyObject **dict)
1585+
{
1586+
PyObject **dict_ptr;
1587+
int res = object_getdictptr(obj, &dict_ptr);
1588+
if (res == 1) {
1589+
assert(*dict_ptr != NULL);
1590+
*dict = Py_NewRef(*dict_ptr);
15631591
}
1564-
return (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict;
1592+
else {
1593+
assert(dict_ptr == NULL);
1594+
*dict = NULL;
1595+
}
1596+
return res;
15651597
}
15661598

1599+
15671600
PyObject *
15681601
PyObject_SelfIter(PyObject *obj)
15691602
{

0 commit comments

Comments
 (0)