diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 0031708c468..86d3967d9fb 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -413,6 +413,20 @@ The following functions and structs are used to create Creating classes whose metaclass overrides :c:member:`~PyTypeObject.tp_new` is no longer allowed. +.. c:function:: int PyType_Freeze(PyTypeObject *type) + + Make a type immutable: set the :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. + + All base classes of *type* must be immutable. + + On success, return ``0``. + On error, set an exception and return ``-1``. + + The type must not be used before it's made immutable. For example, type + instances must not be created before the type is made immutable. + + .. versionadded:: 3.14 + .. raw:: html diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 9314facd2ad..6f9d27297e8 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -684,6 +684,7 @@ func,PyTuple_Size,3.2,, data,PyTuple_Type,3.2,, type,PyTypeObject,3.2,,opaque func,PyType_ClearCache,3.2,, +func,PyType_Freeze,3.14,, func,PyType_FromMetaclass,3.12,, func,PyType_FromModuleAndSpec,3.10,, func,PyType_FromSpec,3.2,, diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 64f3d18e7fc..d95f1848ad6 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -777,6 +777,9 @@ New features (Contributed by Victor Stinner in :gh:`124502`.) +* Add :c:func:`PyType_Freeze` function to make a type immutable. + (Contributed by Victor Stinner in :gh:`121654`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/object.h b/Include/object.h index 7e1b0966fc5..3876d8449af 100644 --- a/Include/object.h +++ b/Include/object.h @@ -796,6 +796,10 @@ static inline int PyType_CheckExact(PyObject *op) { PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030e0000 +PyAPI_FUNC(int) PyType_Freeze(PyTypeObject *type); +#endif + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_capi/test_type.py b/Lib/test/test_capi/test_type.py new file mode 100644 index 00000000000..54c83e09f89 --- /dev/null +++ b/Lib/test/test_capi/test_type.py @@ -0,0 +1,66 @@ +from test.support import import_helper +import unittest + +_testcapi = import_helper.import_module('_testcapi') + + +class TypeTests(unittest.TestCase): + def test_freeze(self): + # test PyType_Freeze() + type_freeze = _testcapi.type_freeze + + # simple case, no inherante + class MyType: + pass + MyType.attr = "mutable" + + type_freeze(MyType) + err_msg = "cannot set 'attr' attribute of immutable type 'MyType'" + with self.assertRaisesRegex(TypeError, err_msg): + # the class is now immutable + MyType.attr = "immutable" + + # test MRO: PyType_Freeze() requires base classes to be immutable + class A: pass + class B: pass + class C(B): pass + class D(A, C): pass + + self.assertEqual(D.mro(), [D, A, C, B, object]) + with self.assertRaises(TypeError): + type_freeze(D) + + type_freeze(A) + type_freeze(B) + type_freeze(C) + # all parent classes are now immutable, so D can be made immutable + # as well + type_freeze(D) + + def test_freeze_meta(self): + """test PyType_Freeze() with overridden MRO""" + type_freeze = _testcapi.type_freeze + + class Base: + value = 1 + + class Meta(type): + def mro(cls): + return (cls, Base, object) + + class FreezeThis(metaclass=Meta): + """This has `Base` in the MRO, but not tp_bases""" + + self.assertEqual(FreezeThis.value, 1) + + with self.assertRaises(TypeError): + type_freeze(FreezeThis) + + Base.value = 2 + self.assertEqual(FreezeThis.value, 2) + + type_freeze(Base) + with self.assertRaises(TypeError): + Base.value = 3 + type_freeze(FreezeThis) + self.assertEqual(FreezeThis.value, 2) diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index b14d500a9c6..fa08dc6a25b 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -713,6 +713,7 @@ SYMBOL_NAMES = ( "PyTuple_Size", "PyTuple_Type", "PyType_ClearCache", + "PyType_Freeze", "PyType_FromMetaclass", "PyType_FromModuleAndSpec", "PyType_FromSpec", diff --git a/Misc/NEWS.d/next/C_API/2024-07-30-14-40-08.gh-issue-121654.tgGeAl.rst b/Misc/NEWS.d/next/C_API/2024-07-30-14-40-08.gh-issue-121654.tgGeAl.rst new file mode 100644 index 00000000000..134d36c281a --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-07-30-14-40-08.gh-issue-121654.tgGeAl.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyType_Freeze` function to make a type immutable. Patch by +Victor Stinner. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 62978261745..f9e51f0683c 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2538,3 +2538,5 @@ added = '3.14' [function.PyUnicode_Equal] added = '3.14' +[function.PyType_Freeze] + added = '3.14' diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index ea26295cca4..26f68691e44 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3310,6 +3310,7 @@ test_critical_sections(PyObject *module, PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } + // Used by `finalize_thread_hang`. #ifdef _POSIX_THREADS static void finalize_thread_hang_cleanup_callback(void *Py_UNUSED(arg)) { @@ -3339,6 +3340,20 @@ finalize_thread_hang(PyObject *self, PyObject *callback) } +static PyObject * +type_freeze(PyObject *module, PyObject *args) +{ + PyTypeObject *type; + if (!PyArg_ParseTuple(args, "O!", &PyType_Type, &type)) { + return NULL; + } + if (PyType_Freeze(type) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -3479,6 +3494,7 @@ static PyMethodDef TestMethods[] = { {"function_set_warning", function_set_warning, METH_NOARGS}, {"test_critical_sections", test_critical_sections, METH_NOARGS}, {"finalize_thread_hang", finalize_thread_hang, METH_O, NULL}, + {"type_freeze", type_freeze, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 4d843824e1e..b4a11195613 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4637,6 +4637,32 @@ check_basicsize_includes_size_and_offsets(PyTypeObject* type) return 1; } +static int +check_immutable_bases(const char *type_name, PyObject *bases, int skip_first) +{ + Py_ssize_t i = 0; + if (skip_first) { + // When testing the MRO, skip the type itself + i = 1; + } + for (; iflags & Py_TPFLAGS_IMMUTABLETYPE) { - for (int i=0; iname, b - ); - goto finally; - } + if (check_immutable_bases(spec->name, bases, 0) < 0) { + goto finally; } } @@ -11319,6 +11334,30 @@ add_operators(PyTypeObject *type) } +int +PyType_Freeze(PyTypeObject *type) +{ + // gh-121654: Check the __mro__ instead of __bases__ + PyObject *mro = type_get_mro(type, NULL); + if (!PyTuple_Check(mro)) { + Py_DECREF(mro); + PyErr_SetString(PyExc_TypeError, "unable to get the type MRO"); + return -1; + } + + int check = check_immutable_bases(type->tp_name, mro, 1); + Py_DECREF(mro); + if (check < 0) { + return -1; + } + + type->tp_flags |= Py_TPFLAGS_IMMUTABLETYPE; + PyType_Modified(type); + + return 0; +} + + /* Cooperative 'super' */ typedef struct { diff --git a/PC/python3dll.c b/PC/python3dll.c index 9296474617e..8657ddb9fa5 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -646,6 +646,7 @@ EXPORT_FUNC(PyTuple_Pack) EXPORT_FUNC(PyTuple_SetItem) EXPORT_FUNC(PyTuple_Size) EXPORT_FUNC(PyType_ClearCache) +EXPORT_FUNC(PyType_Freeze) EXPORT_FUNC(PyType_FromMetaclass) EXPORT_FUNC(PyType_FromModuleAndSpec) EXPORT_FUNC(PyType_FromSpec)