From 1a238a67792493af2b44744de39aca5b4d768a4e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 18:30:43 +0200 Subject: [PATCH 1/4] gh-155503: Add more PyType C API tests Add tests on functions: * PyType_ClearCache() * PyType_GetFlags() * PyType_IsSubtype() * PyType_Ready() Move PyType limited C API tests from _testcapi to _testlimitedcapi. Add a new Modules/_testlimitedcapi/type.c file. --- Include/object.h | 2 +- Lib/test/test_capi/test_type.py | 169 +++++++++++++++- Lib/test/test_type_cache.py | 6 +- Modules/Setup.stdlib.in | 2 +- Modules/_testcapi/type.c | 134 +++++-------- Modules/_testlimitedcapi.c | 3 + Modules/_testlimitedcapi/parts.h | 1 + Modules/_testlimitedcapi/type.c | 244 +++++++++++++++++++++++ PCbuild/_testlimitedcapi.vcxproj | 1 + PCbuild/_testlimitedcapi.vcxproj.filters | 1 + 10 files changed, 467 insertions(+), 96 deletions(-) create mode 100644 Modules/_testlimitedcapi/type.c diff --git a/Include/object.h b/Include/object.h index 20c2dab4401fef..549b91ebecf4f2 100644 --- a/Include/object.h +++ b/Include/object.h @@ -490,7 +490,7 @@ given type object has a specified feature. #define Py_TPFLAGS_SEQUENCE (1 << 5) /* Set if instances of the type object are treated as mappings for pattern matching */ #define Py_TPFLAGS_MAPPING (1 << 6) -#endif +#endif // Py_LIMITED_API /* Disallow creating instances of the type: set tp_new to NULL and don't create * the "__new__" key in the type dictionary. */ diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index e6a8ef9eed6fc4..251f1717382176 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -2,6 +2,7 @@ import unittest _testcapi = import_helper.import_module('_testcapi') +_testlimitedcapi = import_helper.import_module('_testlimitedcapi') class BuiltinStaticTypesTests(unittest.TestCase): @@ -39,15 +40,18 @@ def test_tp_mro_is_set(self): class TypeTests(unittest.TestCase): def test_get_type_name(self): + # Test PyType_GetName(), PyType_GetQualName(), + # PyType_GetFullyQualifiedName() and PyType_GetModuleName(). + class MyType: pass - from _testcapi import ( + from _testlimitedcapi import ( get_type_name, get_type_qualname, get_type_fullyqualname, get_type_module_name) from collections import OrderedDict - ht = _testcapi.get_heaptype_for_name() + ht = _testlimitedcapi.get_heaptype_for_name() for cls, fullname, modname, qualname, name in ( (int, 'int', @@ -215,7 +219,7 @@ class H2(int): pass def test_freeze(self): # test PyType_Freeze() - type_freeze = _testcapi.type_freeze + type_freeze = _testlimitedcapi.type_freeze # simple case, no inherante class MyType: @@ -250,7 +254,7 @@ class D(A, C): pass "Specialization failure triggers gh-127773") def test_freeze_meta(self): """test PyType_Freeze() with overridden MRO""" - type_freeze = _testcapi.type_freeze + type_freeze = _testlimitedcapi.type_freeze class Base: value = 1 @@ -299,3 +303,160 @@ def test_extension_managed_weakref_nogc_type(self): "flag but not Py_TPFLAGS_HAVE_GC flag") with self.assertRaisesRegex(SystemError, msg): _testcapi.create_managed_weakref_nogc_type() + + def test_type_ready(self): + # Test PyType_Ready(): calling it on initialized types + # must not raise an exception. + type_ready = _testlimitedcapi.type_ready + + class HeapType: + pass + + type_ready(int) + type_ready(dict) + type_ready(HeapType) + + def test_type_clearcache(self): + # Test PyType_ClearCache() + type_clearcache = _testlimitedcapi.type_clearcache + version_tag = type_clearcache() + self.assertEqual(type(version_tag), int) + self.assertGreaterEqual(version_tag, 0) + + def test_type_getflags(self): + # Test PyType_GetFlags() + type_getflags = _testlimitedcapi.type_getflags + + from _testlimitedcapi import ( + Py_TPFLAGS_HEAPTYPE, + Py_TPFLAGS_HAVE_GC, + Py_TPFLAGS_HAVE_FINALIZE, + Py_TPFLAGS_HAVE_VERSION_TAG, + Py_TPFLAGS_VALID_VERSION_TAG, + Py_TPFLAGS_HAVE_VECTORCALL, + Py_TPFLAGS_DISALLOW_INSTANTIATION, + Py_TPFLAGS_IMMUTABLETYPE, + Py_TPFLAGS_READY, + Py_TPFLAGS_READYING, + Py_TPFLAGS_LONG_SUBCLASS, + Py_TPFLAGS_LIST_SUBCLASS, + Py_TPFLAGS_TUPLE_SUBCLASS, + Py_TPFLAGS_BYTES_SUBCLASS, + Py_TPFLAGS_UNICODE_SUBCLASS, + Py_TPFLAGS_DICT_SUBCLASS, + Py_TPFLAGS_BASE_EXC_SUBCLASS, + Py_TPFLAGS_TYPE_SUBCLASS, + Py_TPFLAGS_IS_ABSTRACT, + Py_TPFLAGS_BASETYPE, + _Py_TPFLAGS_MATCH_SELF, + Py_TPFLAGS_ITEMS_AT_END, + Py_TPFLAGS_METHOD_DESCRIPTOR, + ) + from _testcapi import ( + _Py_TPFLAGS_STATIC_BUILTIN, + Py_TPFLAGS_SEQUENCE, + Py_TPFLAGS_MAPPING, + Py_TPFLAGS_INLINE_VALUES, + Py_TPFLAGS_MANAGED_WEAKREF, + Py_TPFLAGS_MANAGED_DICT, + ) + + def check_flag(flags, flag, expected): + self.assertEqual(bool(flags & flag), expected) + + def check_subclasses(test_type, flags): + for flag, base_type in ( + (Py_TPFLAGS_LONG_SUBCLASS, int), + (Py_TPFLAGS_LIST_SUBCLASS, list), + (Py_TPFLAGS_TUPLE_SUBCLASS, tuple), + (Py_TPFLAGS_BYTES_SUBCLASS, bytes), + (Py_TPFLAGS_UNICODE_SUBCLASS, str), + (Py_TPFLAGS_DICT_SUBCLASS, dict), + (Py_TPFLAGS_BASE_EXC_SUBCLASS, BaseException), + (Py_TPFLAGS_TYPE_SUBCLASS, type), + ): + with self.subTest(test_type=test_type, flag=flag, base_type=base_type): + check_flag(flags, flag, issubclass(test_type, base_type)) + + def check_type(test_type, static_type, have_gc=False, have_vectorcall=False, + is_base_type=True, sequence=False, mapping=False, + match_self=True, items_at_end=False): + heap_type = not static_type + + flags = type_getflags(test_type) + check_flag(flags, _Py_TPFLAGS_STATIC_BUILTIN, static_type) + check_flag(flags, Py_TPFLAGS_HEAPTYPE, heap_type) + check_flag(flags, Py_TPFLAGS_HAVE_GC, have_gc) + check_subclasses(test_type, flags) + check_flag(flags, Py_TPFLAGS_HAVE_VECTORCALL, have_vectorcall) + check_flag(flags, Py_TPFLAGS_DISALLOW_INSTANTIATION, False) + check_flag(flags, Py_TPFLAGS_IMMUTABLETYPE, static_type) + check_flag(flags, Py_TPFLAGS_READY, True) + check_flag(flags, Py_TPFLAGS_READYING, False) + check_flag(flags, Py_TPFLAGS_IS_ABSTRACT, False) + check_flag(flags, Py_TPFLAGS_BASETYPE, is_base_type) + check_flag(flags, Py_TPFLAGS_SEQUENCE, sequence) + check_flag(flags, Py_TPFLAGS_MAPPING, mapping) + + check_flag(flags, Py_TPFLAGS_INLINE_VALUES, heap_type) + check_flag(flags, Py_TPFLAGS_MANAGED_WEAKREF, heap_type) + check_flag(flags, Py_TPFLAGS_MANAGED_DICT, heap_type) + check_flag(flags, Py_TPFLAGS_ITEMS_AT_END, items_at_end) + check_flag(flags, Py_TPFLAGS_METHOD_DESCRIPTOR, False) + + check_flag(flags, _Py_TPFLAGS_MATCH_SELF, match_self) + + # Flags kept for backward compatibility + check_flag(flags, Py_TPFLAGS_HAVE_FINALIZE, False) + check_flag(flags, Py_TPFLAGS_HAVE_VERSION_TAG, False) + check_flag(flags, Py_TPFLAGS_VALID_VERSION_TAG, False) + + # Scalar types + check_type(int, static_type=True) + check_type(bool, static_type=True, + is_base_type=False) + check_type(float, static_type=True) + check_type(complex, static_type=True, + match_self=False) + check_type(bytes, static_type=True) + check_type(bytearray, static_type=True) + check_type(str, static_type=True) + + # Collection types + check_type(tuple, static_type=True, have_gc=True, + sequence=True) + check_type(list, static_type=True, have_gc=True, + sequence=True) + check_type(dict, static_type=True, have_gc=True, + mapping=True) + check_type(frozendict, static_type=True, have_gc=True, + mapping=True) + check_type(set, static_type=True, have_gc=True) + check_type(frozenset, static_type=True, have_gc=True) + + # Other types + check_type(BaseException, static_type=True, have_gc=True, + match_self=False) + check_type(type, static_type=True, have_gc=True, + have_vectorcall=True, + match_self=False, + items_at_end=True) + + # Heap type + class HeapType: + pass + check_type(HeapType, static_type=False, have_gc=True, match_self=False) + + def test_type_issubtype(self): + # Test PyType_IsSubtype() + type_issubtype = _testlimitedcapi.type_issubtype + + class MyList(list): + pass + + self.assertTrue(type_issubtype(bool, int)) + self.assertTrue(type_issubtype(MyList, list)) + + self.assertFalse(type_issubtype(int, type)) + self.assertFalse(type_issubtype(frozendict, dict)) + self.assertFalse(type_issubtype(MyList, tuple)) diff --git a/Lib/test/test_type_cache.py b/Lib/test/test_type_cache.py index 9827f2498554a5..0031e4b59c67c9 100644 --- a/Lib/test/test_type_cache.py +++ b/Lib/test/test_type_cache.py @@ -11,13 +11,15 @@ except ImportError: _clear_type_cache = None -# Skip this test if the _testcapi module isn't available. +# Skip this test if the _testcapi modules are not available. _testcapi = import_helper.import_module("_testcapi") +_testlimitedcapi = import_helper.import_module("_testlimitedcapi") _testinternalcapi = import_helper.import_module("_testinternalcapi") + type_get_version = _testcapi.type_get_version type_assign_specific_version_unsafe = _testinternalcapi.type_assign_specific_version_unsafe type_assign_version = _testcapi.type_assign_version -type_modified = _testcapi.type_modified +type_modified = _testlimitedcapi.type_modified def clear_type_cache(): with warnings.catch_warnings(): diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index eaf8777a56c59b..440d5a71608ba9 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c -@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c +@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/type.c b/Modules/_testcapi/type.c index f566efa0ca15ae..cc3c3f6d35bbd7 100644 --- a/Modules/_testcapi/type.c +++ b/Modules/_testcapi/type.c @@ -13,50 +13,11 @@ static PyType_Spec HeapTypeNameType_Spec = { .slots = HeapTypeNameType_slots, }; -static PyObject * -get_heaptype_for_name(PyObject *self, PyObject *Py_UNUSED(ignored)) -{ - return PyType_FromSpec(&HeapTypeNameType_Spec); -} - - -static PyObject * -get_type_name(PyObject *self, PyObject *type) -{ - assert(PyType_Check(type)); - return PyType_GetName((PyTypeObject *)type); -} - - -static PyObject * -get_type_qualname(PyObject *self, PyObject *type) -{ - assert(PyType_Check(type)); - return PyType_GetQualName((PyTypeObject *)type); -} - - -static PyObject * -get_type_fullyqualname(PyObject *self, PyObject *type) -{ - assert(PyType_Check(type)); - return PyType_GetFullyQualifiedName((PyTypeObject *)type); -} - - -static PyObject * -get_type_module_name(PyObject *self, PyObject *type) -{ - assert(PyType_Check(type)); - return PyType_GetModuleName((PyTypeObject *)type); -} - +// Test for PyType_GetDict() static PyObject * test_get_type_dict(PyObject *self, PyObject *Py_UNUSED(ignored)) { - /* Test for PyType_GetDict */ - // Assert ints have a `to_bytes` method PyObject *long_dict = PyType_GetDict(&PyLong_Type); assert(long_dict); @@ -77,6 +38,7 @@ test_get_type_dict(PyObject *self, PyObject *Py_UNUSED(ignored)) } +// Test PyType_GetSlot() static PyObject * test_get_statictype_slots(PyObject *self, PyObject *Py_UNUSED(ignored)) { @@ -133,42 +95,40 @@ test_get_statictype_slots(PyObject *self, PyObject *Py_UNUSED(ignored)) } -// Get type->tp_version_tag -static PyObject * -type_get_version(PyObject *self, PyObject *type) +static int +check_type_arg(PyObject *arg) { - if (!PyType_Check(type)) { + if (!PyType_Check(arg)) { PyErr_SetString(PyExc_TypeError, "argument must be a type"); - return NULL; - } - PyObject *res = PyLong_FromUnsignedLong( - ((PyTypeObject *)type)->tp_version_tag); - if (res == NULL) { - assert(PyErr_Occurred()); - return NULL; + return -1; } - return res; + return 0; } + +// Get type->tp_version_tag static PyObject * -type_modified(PyObject *self, PyObject *arg) +type_get_version(PyObject *self, PyObject *arg) { - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); + if (check_type_arg(arg) < 0) { return NULL; } PyTypeObject *type = (PyTypeObject*)arg; - PyType_Modified(type); - Py_RETURN_NONE; + PyObject *res = PyLong_FromUnsignedLong(type->tp_version_tag); + if (res == NULL) { + assert(PyErr_Occurred()); + return NULL; + } + return res; } +// Test PyUnstable_Type_AssignVersionTag() static PyObject * type_assign_version(PyObject *self, PyObject *arg) { - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); + if (check_type_arg(arg) < 0) { return NULL; } PyTypeObject *type = (PyTypeObject*)arg; @@ -178,11 +138,11 @@ type_assign_version(PyObject *self, PyObject *arg) } +// Get PyTypeObject.tp_bases static PyObject * type_get_tp_bases(PyObject *self, PyObject *arg) { - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); + if (check_type_arg(arg) < 0) { return NULL; } PyTypeObject *type = (PyTypeObject*)arg; @@ -194,16 +154,17 @@ type_get_tp_bases(PyObject *self, PyObject *arg) return Py_NewRef(bases); } + +// Get PyTypeObject.tp_mro static PyObject * type_get_tp_mro(PyObject *self, PyObject *arg) { - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); + if (check_type_arg(arg) < 0) { return NULL; } PyTypeObject *type = (PyTypeObject*)arg; - PyObject *mro = ((PyTypeObject *)type)->tp_mro; + PyObject *mro = type->tp_mro; if (mro == NULL) { Py_RETURN_NONE; } @@ -211,41 +172,38 @@ type_get_tp_mro(PyObject *self, PyObject *arg) } -static PyObject * -type_freeze(PyObject *module, PyObject *arg) -{ - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); - return NULL; - } - PyTypeObject *type = (PyTypeObject*)arg; - - if (PyType_Freeze(type) < 0) { - return NULL; - } - Py_RETURN_NONE; -} - - static PyMethodDef test_methods[] = { - {"get_heaptype_for_name", get_heaptype_for_name, METH_NOARGS}, - {"get_type_name", get_type_name, METH_O}, - {"get_type_qualname", get_type_qualname, METH_O}, - {"get_type_fullyqualname", get_type_fullyqualname, METH_O}, - {"get_type_module_name", get_type_module_name, METH_O}, {"test_get_type_dict", test_get_type_dict, METH_NOARGS}, {"test_get_statictype_slots", test_get_statictype_slots, METH_NOARGS}, {"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")}, - {"type_modified", type_modified, METH_O, PyDoc_STR("PyType_Modified")}, {"type_assign_version", type_assign_version, METH_O, PyDoc_STR("PyUnstable_Type_AssignVersionTag")}, {"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}, {NULL}, }; int _PyTestCapi_Init_Type(PyObject *m) { - return PyModule_AddFunctions(m, test_methods); + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + +#define ADD_INT(macro) \ + do { \ + if (PyModule_AddIntConstant(m, #macro, macro) < 0) { \ + return -1; \ + } \ + } while (0) + + // Flags excluded from the limited C API + ADD_INT(_Py_TPFLAGS_STATIC_BUILTIN); + ADD_INT(Py_TPFLAGS_INLINE_VALUES); + ADD_INT(Py_TPFLAGS_MANAGED_WEAKREF); + ADD_INT(Py_TPFLAGS_MANAGED_DICT); + ADD_INT(Py_TPFLAGS_SEQUENCE); + ADD_INT(Py_TPFLAGS_MAPPING); + +#undef ADD_INT + return 0; } diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index 0a562ea9c03110..b602d3219f5a52 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -107,5 +107,8 @@ PyInit__testlimitedcapi(void) if (_PyTestLimitedCAPI_Init_Run(mod) < 0) { return NULL; } + if (_PyTestLimitedCAPI_Init_Type(mod) < 0) { + return NULL; + } return mod; } diff --git a/Modules/_testlimitedcapi/parts.h b/Modules/_testlimitedcapi/parts.h index 32c1bbc1b71c97..35fbdea1d434e0 100644 --- a/Modules/_testlimitedcapi/parts.h +++ b/Modules/_testlimitedcapi/parts.h @@ -48,5 +48,6 @@ int _PyTestLimitedCAPI_Init_Version(PyObject *module); int _PyTestLimitedCAPI_Init_File(PyObject *module); int _PyTestLimitedCAPI_Init_Weakref(PyObject *module); int _PyTestLimitedCAPI_Init_Run(PyObject *module); +int _PyTestLimitedCAPI_Init_Type(PyObject *module); #endif // Py_TESTLIMITEDCAPI_PARTS_H diff --git a/Modules/_testlimitedcapi/type.c b/Modules/_testlimitedcapi/type.c new file mode 100644 index 00000000000000..d1b3bf130fd399 --- /dev/null +++ b/Modules/_testlimitedcapi/type.c @@ -0,0 +1,244 @@ +// Need limited C API version 3.14 for PyType_Freeze() +#include "pyconfig.h" // Py_GIL_DISABLED +#if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API) +# define Py_LIMITED_API 0x030e0000 +#endif + +#include "parts.h" +#include "util.h" + + +static PyType_Slot HeapTypeNameType_slots[] = { + {0}, +}; + +static PyType_Spec HeapTypeNameType_Spec = { + .name = "_testcapi.HeapTypeNameType", + .basicsize = sizeof(PyObject), + .flags = Py_TPFLAGS_DEFAULT, + .slots = HeapTypeNameType_slots, +}; + + +// Test PyType_FromSpec() with a minimum PyType_Spec +static PyObject* +get_heaptype_for_name(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return PyType_FromSpec(&HeapTypeNameType_Spec); +} + + +static int +check_type_arg(PyObject *arg) +{ + if (!PyType_Check(arg)) { + PyErr_SetString(PyExc_TypeError, "argument must be a type"); + return -1; + } + return 0; +} + + +// Test PyType_GetName() +static PyObject* +get_type_name(PyObject *self, PyObject *type) +{ + if (check_type_arg(type) < 0) { + return NULL; + } + return PyType_GetName((PyTypeObject *)type); +} + + +// Test PyType_GetQualName() +static PyObject* +get_type_qualname(PyObject *self, PyObject *type) +{ + if (check_type_arg(type) < 0) { + return NULL; + } + return PyType_GetQualName((PyTypeObject *)type); +} + + +// Test PyType_GetFullyQualifiedName() +static PyObject* +get_type_fullyqualname(PyObject *self, PyObject *type) +{ + if (check_type_arg(type) < 0) { + return NULL; + } + return PyType_GetFullyQualifiedName((PyTypeObject *)type); +} + + +// Test PyType_GetModuleName() +static PyObject* +get_type_module_name(PyObject *self, PyObject *type) +{ + if (check_type_arg(type) < 0) { + return NULL; + } + return PyType_GetModuleName((PyTypeObject *)type); +} + + +// Test PyType_Modified() +static PyObject* +type_modified(PyObject *self, PyObject *arg) +{ + if (check_type_arg(arg) < 0) { + return NULL; + } + PyTypeObject *type = (PyTypeObject*)arg; + + PyType_Modified(type); + Py_RETURN_NONE; +} + + +// Test PyType_Ready() +static PyObject* +type_ready(PyObject *self, PyObject *arg) +{ + assert(!PyErr_Occurred()); + + if (check_type_arg(arg) < 0) { + return NULL; + } + PyTypeObject *type = (PyTypeObject*)arg; + + if (PyType_Ready(type) < 0) { + assert(PyErr_Occurred()); + return NULL; + } + assert(!PyErr_Occurred()); + Py_RETURN_NONE; +} + + +// Test PyType_Freeze() +static PyObject * +type_freeze(PyObject *module, PyObject *arg) +{ + if (check_type_arg(arg) < 0) { + return NULL; + } + PyTypeObject *type = (PyTypeObject*)arg; + + if (PyType_Freeze(type) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + +// Test PyType_ClearCache() +static PyObject * +type_clearcache(PyObject *module, PyObject *Py_UNUSED(arg)) +{ + // Since Python 3.16, PyType_ClearCache() is a no-op as the type cache is + // now implemented per-type. It still returns the current version tag. + unsigned int version_tag = PyType_ClearCache(); + assert(!PyErr_Occurred()); + return PyLong_FromUnsignedLong(version_tag); +} + + +// Test PyType_GetFlags() +static PyObject * +type_getflags(PyObject *module, PyObject *arg) +{ + if (check_type_arg(arg) < 0) { + return NULL; + } + PyTypeObject *type = (PyTypeObject*)arg; + + unsigned long flags = PyType_GetFlags(type); + assert(!PyErr_Occurred()); + return PyLong_FromUnsignedLong(flags); +} + + +// Test PyType_IsSubtype() +static PyObject * +type_issubtype(PyObject *module, PyObject *args) +{ + PyTypeObject *type1, *type2; + if (!PyArg_ParseTuple(args, "O!O!", + &PyType_Type, &type1, + &PyType_Type, &type2)) { + return NULL; + } + + int is_subtype = PyType_IsSubtype(type1, type2); + if (is_subtype != 0 && is_subtype != 1) { + PyErr_Format(PyExc_AssertionError, + "PyType_IsSubtype expected result: %i", + is_subtype); + return NULL; + } + return PyBool_FromLong(is_subtype); +} + + +static PyMethodDef test_methods[] = { + {"get_heaptype_for_name", get_heaptype_for_name, METH_NOARGS}, + {"get_type_name", get_type_name, METH_O}, + {"get_type_qualname", get_type_qualname, METH_O}, + {"get_type_fullyqualname", get_type_fullyqualname, METH_O}, + {"get_type_module_name", get_type_module_name, METH_O}, + {"type_ready", type_ready, METH_O}, + {"type_modified", type_modified, METH_O}, + {"type_freeze", type_freeze, METH_O}, + {"type_clearcache", type_clearcache, METH_NOARGS}, + {"type_getflags", type_getflags, METH_O}, + {"type_issubtype", type_issubtype, METH_VARARGS}, + {NULL}, +}; + +int +_PyTestLimitedCAPI_Init_Type(PyObject *m) +{ + if (PyModule_AddFunctions(m, test_methods) < 0) { + return 0; + } + +#define ADD_INT(macro) \ + do { \ + if (PyModule_AddIntConstant(m, #macro, macro) < 0) { \ + return -1; \ + } \ + } while (0) + + ADD_INT(Py_TPFLAGS_DEFAULT); + + ADD_INT(Py_TPFLAGS_HAVE_FINALIZE); + ADD_INT(Py_TPFLAGS_HAVE_GC); + ADD_INT(Py_TPFLAGS_HAVE_VERSION_TAG); + ADD_INT(Py_TPFLAGS_HAVE_VECTORCALL); + + ADD_INT(Py_TPFLAGS_DISALLOW_INSTANTIATION ); + ADD_INT(Py_TPFLAGS_IMMUTABLETYPE); + ADD_INT(Py_TPFLAGS_HEAPTYPE); + ADD_INT(Py_TPFLAGS_BASETYPE); + ADD_INT(Py_TPFLAGS_READY); + ADD_INT(Py_TPFLAGS_READYING); + ADD_INT(Py_TPFLAGS_METHOD_DESCRIPTOR); + ADD_INT(Py_TPFLAGS_VALID_VERSION_TAG); + ADD_INT(Py_TPFLAGS_IS_ABSTRACT); + ADD_INT(_Py_TPFLAGS_MATCH_SELF); + ADD_INT(Py_TPFLAGS_ITEMS_AT_END); + + ADD_INT(Py_TPFLAGS_LONG_SUBCLASS); + ADD_INT(Py_TPFLAGS_LIST_SUBCLASS); + ADD_INT(Py_TPFLAGS_TUPLE_SUBCLASS); + ADD_INT(Py_TPFLAGS_BYTES_SUBCLASS); + ADD_INT(Py_TPFLAGS_UNICODE_SUBCLASS); + ADD_INT(Py_TPFLAGS_DICT_SUBCLASS); + ADD_INT(Py_TPFLAGS_BASE_EXC_SUBCLASS); + ADD_INT(Py_TPFLAGS_TYPE_SUBCLASS); + +#undef ADD_INT + return 0; +} diff --git a/PCbuild/_testlimitedcapi.vcxproj b/PCbuild/_testlimitedcapi.vcxproj index 785bb151e08129..c2bdea923f0f6d 100644 --- a/PCbuild/_testlimitedcapi.vcxproj +++ b/PCbuild/_testlimitedcapi.vcxproj @@ -120,6 +120,7 @@ + diff --git a/PCbuild/_testlimitedcapi.vcxproj.filters b/PCbuild/_testlimitedcapi.vcxproj.filters index 51dc9950a10376..4c999949474b97 100644 --- a/PCbuild/_testlimitedcapi.vcxproj.filters +++ b/PCbuild/_testlimitedcapi.vcxproj.filters @@ -36,6 +36,7 @@ + From b258994b1e6d0e581c1679bc08437f0a38f129cf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 10 Aug 2026 23:32:40 +0200 Subject: [PATCH 2/4] Address Serhiy's review * Accept None: treat None as NULL * Fix typo in _PyTestLimitedCAPI_Init_Type(): return -1 on error. * Add test_type_modified(). * Move PyType_IsSubtype() result check to Python. --- Lib/test/test_capi/test_type.py | 30 ++++++++++++++++++- Modules/_testcapi/type.c | 7 +++++ Modules/_testlimitedcapi/type.c | 53 ++++++++++++++++++++------------- 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 251f1717382176..090b31a5d5cec2 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -4,6 +4,8 @@ _testcapi = import_helper.import_module('_testcapi') _testlimitedcapi = import_helper.import_module('_testlimitedcapi') +NULL = None + class BuiltinStaticTypesTests(unittest.TestCase): @@ -111,6 +113,11 @@ class MyType: MyType.__module__ = 123 self.assertEqual(get_type_fullyqualname(MyType), 'my_qualname') + # CRASHES get_type_name(NULL) + # CRASHES get_type_qualname(NULL) + # CRASHES get_type_fullyqualname(NULL) + # CRASHES get_type_module_name(NULL) + def test_get_base_by_token(self): def get_base_by_token(src, key, comparable=True): def run(use_mro): @@ -249,6 +256,8 @@ class D(A, C): pass # as well type_freeze(D) + # CRASHES type_freeze(NULL) + @unittest.skipIf( Py_GIL_DISABLED and refleak_helper.hunting_for_refleaks(), "Specialization failure triggers gh-127773") @@ -316,6 +325,8 @@ class HeapType: type_ready(dict) type_ready(HeapType) + # CRASHES type_ready(NULL) + def test_type_clearcache(self): # Test PyType_ClearCache() type_clearcache = _testlimitedcapi.type_clearcache @@ -447,9 +458,16 @@ class HeapType: pass check_type(HeapType, static_type=False, have_gc=True, match_self=False) + # CRASHES type_getflags(NULL) + def test_type_issubtype(self): # Test PyType_IsSubtype() - type_issubtype = _testlimitedcapi.type_issubtype + _type_issubtype = _testlimitedcapi.type_issubtype + + def type_issubtype(type1, type2): + res = _type_issubtype(type1, type2) + self.assertIn(res, (0, 1)) + return bool(res) class MyList(list): pass @@ -460,3 +478,13 @@ class MyList(list): self.assertFalse(type_issubtype(int, type)) self.assertFalse(type_issubtype(frozendict, dict)) self.assertFalse(type_issubtype(MyList, tuple)) + + def test_type_modified(self): + # Test PyType_Modified() + type_modified = _testlimitedcapi.type_modified + + class MyType: + pass + type_modified(MyType) + + # CRASHES type_modified(NULL) diff --git a/Modules/_testcapi/type.c b/Modules/_testcapi/type.c index cc3c3f6d35bbd7..17b6a551e2a970 100644 --- a/Modules/_testcapi/type.c +++ b/Modules/_testcapi/type.c @@ -98,6 +98,9 @@ test_get_statictype_slots(PyObject *self, PyObject *Py_UNUSED(ignored)) static int check_type_arg(PyObject *arg) { + if (arg == NULL) { + return 0; + } if (!PyType_Check(arg)) { PyErr_SetString(PyExc_TypeError, "argument must be a type"); return -1; @@ -110,6 +113,7 @@ check_type_arg(PyObject *arg) static PyObject * type_get_version(PyObject *self, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -128,6 +132,7 @@ type_get_version(PyObject *self, PyObject *arg) static PyObject * type_assign_version(PyObject *self, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -142,6 +147,7 @@ type_assign_version(PyObject *self, PyObject *arg) static PyObject * type_get_tp_bases(PyObject *self, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -159,6 +165,7 @@ type_get_tp_bases(PyObject *self, PyObject *arg) static PyObject * type_get_tp_mro(PyObject *self, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } diff --git a/Modules/_testlimitedcapi/type.c b/Modules/_testlimitedcapi/type.c index d1b3bf130fd399..b3d435dc25bc23 100644 --- a/Modules/_testlimitedcapi/type.c +++ b/Modules/_testlimitedcapi/type.c @@ -31,6 +31,9 @@ get_heaptype_for_name(PyObject *self, PyObject *Py_UNUSED(ignored)) static int check_type_arg(PyObject *arg) { + if (arg == NULL) { + return 0; + } if (!PyType_Check(arg)) { PyErr_SetString(PyExc_TypeError, "argument must be a type"); return -1; @@ -41,45 +44,57 @@ check_type_arg(PyObject *arg) // Test PyType_GetName() static PyObject* -get_type_name(PyObject *self, PyObject *type) +get_type_name(PyObject *self, PyObject *arg) { - if (check_type_arg(type) < 0) { + NULLABLE(arg); + if (check_type_arg(arg) < 0) { return NULL; } - return PyType_GetName((PyTypeObject *)type); + PyTypeObject *type = (PyTypeObject*)arg; + + return PyType_GetName(type); } // Test PyType_GetQualName() static PyObject* -get_type_qualname(PyObject *self, PyObject *type) +get_type_qualname(PyObject *self, PyObject *arg) { - if (check_type_arg(type) < 0) { + NULLABLE(arg); + if (check_type_arg(arg) < 0) { return NULL; } - return PyType_GetQualName((PyTypeObject *)type); + PyTypeObject *type = (PyTypeObject*)arg; + + return PyType_GetQualName(type); } // Test PyType_GetFullyQualifiedName() static PyObject* -get_type_fullyqualname(PyObject *self, PyObject *type) +get_type_fullyqualname(PyObject *self, PyObject *arg) { - if (check_type_arg(type) < 0) { + NULLABLE(arg); + if (check_type_arg(arg) < 0) { return NULL; } - return PyType_GetFullyQualifiedName((PyTypeObject *)type); + PyTypeObject *type = (PyTypeObject*)arg; + + return PyType_GetFullyQualifiedName(type); } // Test PyType_GetModuleName() static PyObject* -get_type_module_name(PyObject *self, PyObject *type) +get_type_module_name(PyObject *self, PyObject *arg) { - if (check_type_arg(type) < 0) { + NULLABLE(arg); + if (check_type_arg(arg) < 0) { return NULL; } - return PyType_GetModuleName((PyTypeObject *)type); + PyTypeObject *type = (PyTypeObject*)arg; + + return PyType_GetModuleName(type); } @@ -87,6 +102,7 @@ get_type_module_name(PyObject *self, PyObject *type) static PyObject* type_modified(PyObject *self, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -103,6 +119,7 @@ type_ready(PyObject *self, PyObject *arg) { assert(!PyErr_Occurred()); + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -121,6 +138,7 @@ type_ready(PyObject *self, PyObject *arg) static PyObject * type_freeze(PyObject *module, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -149,6 +167,7 @@ type_clearcache(PyObject *module, PyObject *Py_UNUSED(arg)) static PyObject * type_getflags(PyObject *module, PyObject *arg) { + NULLABLE(arg); if (check_type_arg(arg) < 0) { return NULL; } @@ -172,13 +191,7 @@ type_issubtype(PyObject *module, PyObject *args) } int is_subtype = PyType_IsSubtype(type1, type2); - if (is_subtype != 0 && is_subtype != 1) { - PyErr_Format(PyExc_AssertionError, - "PyType_IsSubtype expected result: %i", - is_subtype); - return NULL; - } - return PyBool_FromLong(is_subtype); + return PyLong_FromLong(is_subtype); } @@ -201,7 +214,7 @@ int _PyTestLimitedCAPI_Init_Type(PyObject *m) { if (PyModule_AddFunctions(m, test_methods) < 0) { - return 0; + return -1; } #define ADD_INT(macro) \ From 46fe8cbcca5d0e5ebb65b654f41b4c7f903233ad Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 11 Aug 2026 15:11:57 +0200 Subject: [PATCH 3/4] Remove PyType_Check() --- Modules/_testcapi/type.c | 30 +++-------------------- Modules/_testlimitedcapi/type.c | 43 +++------------------------------ 2 files changed, 8 insertions(+), 65 deletions(-) diff --git a/Modules/_testcapi/type.c b/Modules/_testcapi/type.c index 17b6a551e2a970..91203c7bf0f678 100644 --- a/Modules/_testcapi/type.c +++ b/Modules/_testcapi/type.c @@ -1,3 +1,7 @@ +// Thin wrappers to PyType functions. +// Do no check PyType_Check() so Python tests can pass arbitrary objects, +// even if it's likely to crash. + #include "parts.h" #include "util.h" @@ -95,28 +99,11 @@ test_get_statictype_slots(PyObject *self, PyObject *Py_UNUSED(ignored)) } -static int -check_type_arg(PyObject *arg) -{ - if (arg == NULL) { - return 0; - } - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); - return -1; - } - return 0; -} - - // Get type->tp_version_tag static PyObject * type_get_version(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; PyObject *res = PyLong_FromUnsignedLong(type->tp_version_tag); @@ -133,9 +120,6 @@ static PyObject * type_assign_version(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; int res = PyUnstable_Type_AssignVersionTag(type); @@ -148,9 +132,6 @@ static PyObject * type_get_tp_bases(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; PyObject *bases = type->tp_bases; @@ -166,9 +147,6 @@ static PyObject * type_get_tp_mro(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; PyObject *mro = type->tp_mro; diff --git a/Modules/_testlimitedcapi/type.c b/Modules/_testlimitedcapi/type.c index b3d435dc25bc23..640468ae08dcbb 100644 --- a/Modules/_testlimitedcapi/type.c +++ b/Modules/_testlimitedcapi/type.c @@ -1,3 +1,7 @@ +// Thin wrappers to PyType functions. +// Do no check PyType_Check() so Python tests can pass arbitrary objects, +// even if it's likely to crash. + // Need limited C API version 3.14 for PyType_Freeze() #include "pyconfig.h" // Py_GIL_DISABLED #if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API) @@ -28,28 +32,11 @@ get_heaptype_for_name(PyObject *self, PyObject *Py_UNUSED(ignored)) } -static int -check_type_arg(PyObject *arg) -{ - if (arg == NULL) { - return 0; - } - if (!PyType_Check(arg)) { - PyErr_SetString(PyExc_TypeError, "argument must be a type"); - return -1; - } - return 0; -} - - // Test PyType_GetName() static PyObject* get_type_name(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; return PyType_GetName(type); @@ -61,9 +48,6 @@ static PyObject* get_type_qualname(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; return PyType_GetQualName(type); @@ -75,9 +59,6 @@ static PyObject* get_type_fullyqualname(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; return PyType_GetFullyQualifiedName(type); @@ -89,9 +70,6 @@ static PyObject* get_type_module_name(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; return PyType_GetModuleName(type); @@ -103,9 +81,6 @@ static PyObject* type_modified(PyObject *self, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; PyType_Modified(type); @@ -118,11 +93,7 @@ static PyObject* type_ready(PyObject *self, PyObject *arg) { assert(!PyErr_Occurred()); - NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; if (PyType_Ready(type) < 0) { @@ -139,9 +110,6 @@ static PyObject * type_freeze(PyObject *module, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; if (PyType_Freeze(type) < 0) { @@ -168,9 +136,6 @@ static PyObject * type_getflags(PyObject *module, PyObject *arg) { NULLABLE(arg); - if (check_type_arg(arg) < 0) { - return NULL; - } PyTypeObject *type = (PyTypeObject*)arg; unsigned long flags = PyType_GetFlags(type); From 045c4fa276fad98b535552c0fe78a6bfa09f6c43 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 11 Aug 2026 22:05:28 +0200 Subject: [PATCH 4/4] Add "CRASHES ... argument must be a type" comments --- Lib/test/test_capi/test_type.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py index 090b31a5d5cec2..90b1f57d1c3c9d 100644 --- a/Lib/test/test_capi/test_type.py +++ b/Lib/test/test_capi/test_type.py @@ -117,6 +117,10 @@ class MyType: # CRASHES get_type_qualname(NULL) # CRASHES get_type_fullyqualname(NULL) # CRASHES get_type_module_name(NULL) + # CRASHES get_type_name(object()): argument must be a type + # CRASHES get_type_qualname(object()): argument must be a type + # CRASHES get_type_fullyqualname(object()): argument must be a type + # CRASHES get_type_module_name(object()): argument must be a type def test_get_base_by_token(self): def get_base_by_token(src, key, comparable=True): @@ -257,6 +261,7 @@ class D(A, C): pass type_freeze(D) # CRASHES type_freeze(NULL) + # CRASHES type_freeze(object()): argument must be a type @unittest.skipIf( Py_GIL_DISABLED and refleak_helper.hunting_for_refleaks(), @@ -326,6 +331,7 @@ class HeapType: type_ready(HeapType) # CRASHES type_ready(NULL) + # CRASHES type_ready(123): argument must be a type def test_type_clearcache(self): # Test PyType_ClearCache() @@ -488,3 +494,4 @@ class MyType: type_modified(MyType) # CRASHES type_modified(NULL) + # CRASHES type_modified({}): argument must be a type