From 480faa664ad737226d0e41bb7e94a6055e12dc94 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 9 Oct 2025 15:39:26 +0200 Subject: [PATCH 1/7] gh-139847: Add PyType_Lookup() function --- Doc/c-api/type.rst | 15 +++++++++++ .../c-api-pending-removal-in-3.19.rst | 13 ++++++++++ Doc/whatsnew/3.13.rst | 2 ++ Doc/whatsnew/3.14.rst | 2 ++ Doc/whatsnew/3.15.rst | 15 +++++++++++ Include/cpython/object.h | 14 +++++++--- Lib/test/test_capi/test_type.py | 17 ++++++++++++ ...-10-09-15-43-15.gh-issue-139847.dZb1v_.rst | 2 ++ ...-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst | 3 +++ Modules/_testcapi/gc.c | 8 +++++- Modules/_testcapi/type.c | 26 +++++++++++++++++++ Objects/typeobject.c | 14 ++++++++++ 12 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 Doc/deprecations/c-api-pending-removal-in-3.19.rst create mode 100644 Misc/NEWS.d/next/C_API/2025-10-09-15-43-15.gh-issue-139847.dZb1v_.rst create mode 100644 Misc/NEWS.d/next/C_API/2025-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 5bdbff4e0ad990..28f8b04554b627 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -306,6 +306,21 @@ 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 `. + + *name* must be a :class:`str`. + + * If found, set *\*attr* to a strong reference and return ``1``. + * If not found, set *\*attr* to ``NULL`` and return ``0``. + * On error, set an exception and return ``-1``. + + .. versionadded:: next + + .. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type) Attempt to assign a version tag to the given type. diff --git a/Doc/deprecations/c-api-pending-removal-in-3.19.rst b/Doc/deprecations/c-api-pending-removal-in-3.19.rst new file mode 100644 index 00000000000000..3adafe6eeec496 --- /dev/null +++ b/Doc/deprecations/c-api-pending-removal-in-3.19.rst @@ -0,0 +1,13 @@ +Pending removal in Python 3.19 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The following private functions are deprecated + and planned for removal in Python 3.19: + + * :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 + these new public functions on Python 3.14 and older. + (Contributed by Victor Stinner in :gh:`128863`.) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 1548f128b5bdac..821e605d82bd13 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -2554,6 +2554,8 @@ Deprecated C APIs .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst +.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst + .. include:: ../deprecations/c-api-pending-removal-in-future.rst .. _pythoncapi-compat project: https://github.com/python/pythoncapi-compat/ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 677365c2f5994b..0c9384009d4d39 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -3079,6 +3079,8 @@ Deprecated C APIs .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst +.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst + .. include:: ../deprecations/c-api-pending-removal-in-future.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 4b176d6c8e6034..82f94c1c98042c 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: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 ---------------------- @@ -869,6 +873,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. @@ -915,6 +922,14 @@ Deprecated C APIs .. Add C API deprecations above alphabetically, not here at the end. +.. include:: ../deprecations/c-api-pending-removal-in-3.16.rst + +.. include:: ../deprecations/c-api-pending-removal-in-3.18.rst + +.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst + +.. include:: ../deprecations/c-api-pending-removal-in-future.rst + Removed C APIs -------------- 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..5847371429b72b 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -274,3 +274,20 @@ 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 Parent: + parent_attr = "parent" + + class Child(Parent): + child_attr = "child" + + self.assertEqual(type_lookup(Child, "parent_attr"), "parent") + self.assertEqual(type_lookup(Child, "child_attr"), "child") + 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..6b0ddd0c90650c --- /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_LookupRef` +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) From 1303db26a4ffeb0fe13ec667cec75017d938d1e8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 9 Oct 2025 15:56:34 +0200 Subject: [PATCH 2/7] Fix the function name in the NEWS entry --- .../next/C_API/2025-10-09-15-44-22.gh-issue-139847.-1_Pnx.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6b0ddd0c90650c..c1037aa9afce09 100644 --- 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 @@ -1,3 +1,3 @@ Deprecate the private functions :c:func:`!_PyType_Lookup` and -:c:func:`!_PyType_LookupRef`: use the new public :c:func:`PyType_LookupRef` +:c:func:`!_PyType_LookupRef`: use the new public :c:func:`PyType_Lookup` function instead. Patch by Victor Stinner. From 1c3e523483bbb4bd77cece4a1152364291fa8b49 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 9 Oct 2025 20:49:32 +0200 Subject: [PATCH 3/7] Update Doc/c-api/type.rst Co-authored-by: Sam Gross --- Doc/c-api/type.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 28f8b04554b627..6161478adb6fb7 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -314,7 +314,7 @@ Type Objects *name* must be a :class:`str`. - * If found, set *\*attr* to a strong reference and return ``1``. + * 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``. From 8f24f7a1f30262d74c093560a1477d46f5811d0c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 9 Oct 2025 23:27:55 +0200 Subject: [PATCH 4/7] Doc: link to PyObject_GetOptionalAttr() --- Doc/c-api/object.rst | 3 +++ Doc/c-api/type.rst | 3 +++ 2 files changed, 6 insertions(+) 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 6161478adb6fb7..7defe9dc5b1929 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -320,6 +320,9 @@ Type Objects .. versionadded:: next + .. seealso:: + The :c:func:`PyObject_GetOptionalAttr` function. + .. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type) From c3b9324fd417518773d75e8ac6b23ba113d5ed71 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 11 Oct 2025 19:21:53 +0200 Subject: [PATCH 5/7] Don't schedule _PyType_Lookup() removal in this PR --- Doc/deprecations/c-api-pending-removal-in-3.19.rst | 13 ------------- Doc/whatsnew/3.13.rst | 2 -- Doc/whatsnew/3.14.rst | 2 -- Doc/whatsnew/3.15.rst | 2 -- 4 files changed, 19 deletions(-) delete mode 100644 Doc/deprecations/c-api-pending-removal-in-3.19.rst diff --git a/Doc/deprecations/c-api-pending-removal-in-3.19.rst b/Doc/deprecations/c-api-pending-removal-in-3.19.rst deleted file mode 100644 index 3adafe6eeec496..00000000000000 --- a/Doc/deprecations/c-api-pending-removal-in-3.19.rst +++ /dev/null @@ -1,13 +0,0 @@ -Pending removal in Python 3.19 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -* The following private functions are deprecated - and planned for removal in Python 3.19: - - * :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 - these new public functions on Python 3.14 and older. - (Contributed by Victor Stinner in :gh:`128863`.) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 02354a30aeb79d..499f6f0ff4eaca 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -2552,8 +2552,6 @@ Deprecated C APIs .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst -.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst - .. include:: ../deprecations/c-api-pending-removal-in-future.rst .. _pythoncapi-compat project: https://github.com/python/pythoncapi-compat/ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 0c9384009d4d39..677365c2f5994b 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -3079,8 +3079,6 @@ Deprecated C APIs .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst -.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst - .. include:: ../deprecations/c-api-pending-removal-in-future.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3ea3b93e13b072..20c938fceeffb5 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -929,8 +929,6 @@ Deprecated C APIs .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst -.. include:: ../deprecations/c-api-pending-removal-in-3.19.rst - .. include:: ../deprecations/c-api-pending-removal-in-future.rst Removed C APIs From 01a141ffc1097aeeb8d95c60c9c05732db8b29e2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 11 Oct 2025 19:23:57 +0200 Subject: [PATCH 6/7] Revert Doc/whatsnew/3.15.rst changes --- Doc/whatsnew/3.15.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 20c938fceeffb5..4f1924e0c9590e 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -925,12 +925,6 @@ Deprecated C APIs .. Add C API deprecations above alphabetically, not here at the end. -.. include:: ../deprecations/c-api-pending-removal-in-3.16.rst - -.. include:: ../deprecations/c-api-pending-removal-in-3.18.rst - -.. include:: ../deprecations/c-api-pending-removal-in-future.rst - Removed C APIs -------------- From 12901716ed4964ba39fd257469119c855f95db1e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 16:25:54 +0200 Subject: [PATCH 7/7] Do not invoke descriptors or metaclass __getattr__() Add Python pseudo-code in the doc. --- Doc/c-api/type.rst | 17 +++++++++++++++++ Lib/test/test_capi/test_type.py | 23 ++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 7defe9dc5b1929..1527ca6b457931 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -311,6 +311,7 @@ Type Objects Look for a type attribute through the type :term:`MRO `. + Do not invoke descriptors or metaclass ``__getattr__``. *name* must be a :class:`str`. @@ -318,6 +319,22 @@ Type Objects * 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:: diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 5847371429b72b..a9dea76cd80f2b 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -278,14 +278,31 @@ def test_extension_managed_dict_type(self): def test_type_lookup(self): type_lookup = _testcapi.type_lookup - class Parent: + 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): - child_attr = "child" + attr = "child" + descr = Ten() self.assertEqual(type_lookup(Child, "parent_attr"), "parent") - self.assertEqual(type_lookup(Child, "child_attr"), "child") + 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