diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 78599e704b1317..02beb1abce27ef 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -167,6 +167,9 @@ Object Protocol .. versionadded:: 3.13 + .. seealso:: + The :c:func:`PyType_Lookup` function. + .. c:function:: int PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result); diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 5bdbff4e0ad990..1527ca6b457931 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -306,6 +306,41 @@ Type Objects .. versionadded:: 3.14 + +.. c:function:: int PyType_Lookup(PyTypeObject *type, PyObject *name, PyObject **attr) + + Look for a type attribute through the type + :term:`MRO `. + Do not invoke descriptors or metaclass ``__getattr__``. + + *name* must be a :class:`str`. + + * If found, set *\*attr* to a :term:`strong reference` and return ``1``. + * If not found, set *\*attr* to ``NULL`` and return ``0``. + * On error, set an exception and return ``-1``. + + Python pseudo-code (return :exc:`AttributeError` if not found instead of + raising an exception):: + + def PyType_Lookup(type, name): + if not isinstance(name, str): + raise TypeError + + for klass in type.mro(): + try: + return klass.__dict__[name] + except KeyError: + pass + + # not found + return AttributeError + + .. versionadded:: next + + .. seealso:: + The :c:func:`PyObject_GetOptionalAttr` function. + + .. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type) Attempt to assign a version tag to the given type. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 40286d4fe857e8..4f1924e0c9590e 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -855,6 +855,10 @@ New features * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) +* Add :c:func:`PyType_Lookup` function to look for a type attribute through the + type :term:`MRO `. + (Contributed by Victor Stinner in :gh:`139847`.) + Porting to Python 3.15 ---------------------- @@ -872,6 +876,9 @@ Porting to Python 3.15 * Private functions promoted to public C APIs: + * :c:func:`!_PyType_Lookup`: use :c:func:`PyType_Lookup`. + * :c:func:`!_PyType_LookupRef`: use :c:func:`PyType_Lookup`. + The |pythoncapi_compat_project| can be used to get most of these new functions on Python 3.14 and older. diff --git a/Include/cpython/object.h b/Include/cpython/object.h index e2f87524c218b6..62b0d8f8da0708 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -252,7 +252,7 @@ struct _specialization_cache { // by the specialization machinery, and are invalidated by PyType_Modified. // The rules for using them are as follows: // - If getitem is non-NULL, then it is the same Python function that - // PyType_Lookup(cls, "__getitem__") would return. + // _PyType_Lookup(cls, "__getitem__") would return. // - If getitem is NULL, then getitem_version is meaningless. // - If getitem->func_version == getitem_version, then getitem can be called // with two positional arguments and no keyword arguments, and has neither @@ -289,8 +289,16 @@ typedef struct _heaptypeobject { } PyHeapTypeObject; PyAPI_FUNC(const char *) _PyType_Name(PyTypeObject *); -PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *); -PyAPI_FUNC(PyObject *) _PyType_LookupRef(PyTypeObject *, PyObject *); +PyAPI_FUNC(int) PyType_Lookup( + PyTypeObject *type, + PyObject *name, + PyObject **attr); +_Py_DEPRECATED_EXTERNALLY(3.15) PyAPI_FUNC(PyObject *) _PyType_Lookup( + PyTypeObject *type, + PyObject *name); +_Py_DEPRECATED_EXTERNALLY(3.15) PyAPI_FUNC(PyObject *) _PyType_LookupRef( + PyTypeObject *type, + PyObject *name); PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *); PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 15fb4a93e2ad74..a9dea76cd80f2b 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -274,3 +274,37 @@ def test_extension_managed_dict_type(self): obj.__dict__ = {'bar': 3} self.assertEqual(obj.__dict__, {'bar': 3}) self.assertEqual(obj.bar, 3) + + def test_type_lookup(self): + type_lookup = _testcapi.type_lookup + + class Meta(type): + def __getattr__(self, name): + if name == "meta_attr": + return "meta attr" + else: + raise AttributeError + + class Ten: + def __get__(self, obj, objtype=None): + return 10 + + class Parent(metaclass=Meta): + parent_attr = "parent" + attr = "parent" + + class Child(Parent): + attr = "child" + descr = Ten() + + self.assertEqual(type_lookup(Child, "parent_attr"), "parent") + self.assertEqual(type_lookup(Child, "attr"), "child") + self.assertEqual(type_lookup(Child, "descr"), Child.__dict__['descr']) + self.assertEqual(Child.descr, 10) + self.assertEqual(type_lookup(Child, "meta_attr"), AttributeError) + self.assertEqual(Child.meta_attr, "meta attr") + self.assertEqual(type_lookup(Child, "xxx"), AttributeError) + + # name parameter must be a str + self.assertRaises(TypeError, type_lookup, Child, b'name') + self.assertRaises(TypeError, type_lookup, Child, 123) diff --git a/Misc/NEWS.d/next/C_API/2025-10-09-15-43-15.gh-issue-139847.dZb1v_.rst b/Misc/NEWS.d/next/C_API/2025-10-09-15-43-15.gh-issue-139847.dZb1v_.rst new file mode 100644 index 00000000000000..7edf1e795749d9 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-09-15-43-15.gh-issue-139847.dZb1v_.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyType_Lookup` function to look for a type attribute through +the type :term:`MRO `. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/C_API/2025-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst b/Misc/NEWS.d/next/C_API/2025-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst new file mode 100644 index 00000000000000..c1037aa9afce09 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst @@ -0,0 +1,3 @@ +Deprecate the private functions :c:func:`!_PyType_Lookup` and +:c:func:`!_PyType_LookupRef`: use the new public :c:func:`PyType_Lookup` +function instead. Patch by Victor Stinner. diff --git a/Modules/_testcapi/gc.c b/Modules/_testcapi/gc.c index 863cb52980f942..3d87b80468a896 100644 --- a/Modules/_testcapi/gc.c +++ b/Modules/_testcapi/gc.c @@ -98,8 +98,14 @@ slot_tp_del(PyObject *self) PyErr_SetRaisedException(exc); return; } + /* Execute __del__ method, if any. */ - del = _PyType_LookupRef(Py_TYPE(self), tp_del); + if (PyType_Lookup(Py_TYPE(self), tp_del, &del) < 0) { + Py_DECREF(tp_del); + PyErr_FormatUnraisable("Exception ignored while deallocating"); + PyErr_SetRaisedException(exc); + return; + } Py_DECREF(tp_del); if (del != NULL) { res = PyObject_CallOneArg(del, self); diff --git a/Modules/_testcapi/type.c b/Modules/_testcapi/type.c index 9bef58d1f83668..bb442658eab279 100644 --- a/Modules/_testcapi/type.c +++ b/Modules/_testcapi/type.c @@ -227,6 +227,31 @@ type_freeze(PyObject *module, PyObject *arg) } +static PyObject * +type_lookup(PyObject *self, PyObject *args) +{ + PyTypeObject *type; + PyObject *name, *attr = UNINITIALIZED_PTR; + if (!PyArg_ParseTuple(args, "O!O", &PyType_Type, &type, &name)) { + return NULL; + } + NULLABLE(name); + + switch (PyType_Lookup(type, name, &attr)) { + case -1: + assert(attr == NULL); + return NULL; + case 0: + assert(attr == NULL); + return Py_NewRef(PyExc_AttributeError); + case 1: + return attr; + default: + Py_FatalError("PyType_Lookup() returned invalid code"); + Py_UNREACHABLE(); + } +} + static PyMethodDef test_methods[] = { {"get_heaptype_for_name", get_heaptype_for_name, METH_NOARGS}, {"get_type_name", get_type_name, METH_O}, @@ -241,6 +266,7 @@ static PyMethodDef test_methods[] = { {"type_get_tp_bases", type_get_tp_bases, METH_O}, {"type_get_tp_mro", type_get_tp_mro, METH_O}, {"type_freeze", type_freeze, METH_O}, + {"type_lookup", type_lookup, METH_VARARGS}, {NULL}, }; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 9398bcb29c83e4..31914308d5f569 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6202,6 +6202,20 @@ _PyType_Lookup(PyTypeObject *type, PyObject *name) return res; } +int +PyType_Lookup(PyTypeObject *type, PyObject *name, PyObject **attr) +{ + if (!PyUnicode_Check(name)) { + PyErr_Format(PyExc_TypeError, "name must be a str, got %T", name); + *attr = NULL; + return -1; + } + + assert(PyType_Check(type)); + *attr = _PyType_LookupRefAndVersion(type, name, NULL); + return (*attr != NULL); +} + int _PyType_CacheInitForSpecialization(PyHeapTypeObject *type, PyObject *init, unsigned int tp_version)