]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-121654: Add PyType_Freeze() function (#122457)
authorVictor Stinner <vstinner@python.org>
Fri, 25 Oct 2024 09:12:48 +0000 (11:12 +0200)
committerGitHub <noreply@github.com>
Fri, 25 Oct 2024 09:12:48 +0000 (11:12 +0200)
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Doc/c-api/type.rst
Doc/data/stable_abi.dat
Doc/whatsnew/3.14.rst
Include/object.h
Lib/test/test_capi/test_type.py [new file with mode: 0644]
Lib/test/test_stable_abi_ctypes.py
Misc/NEWS.d/next/C_API/2024-07-30-14-40-08.gh-issue-121654.tgGeAl.rst [new file with mode: 0644]
Misc/stable_abi.toml
Modules/_testcapimodule.c
Objects/typeobject.c
PC/python3dll.c

index 0031708c4680cc847863ff4a31e617c6669bb4ea..86d3967d9fb57767503a25474194e2b80c125c36 100644 (file)
@@ -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
 
    <!-- Keep old URL fragments working (see gh-97908) -->
index 9314facd2ad8730fcf4f80e8325a94ac7cff7307..6f9d27297e8f658f0cc5b2fcf1ae2ba6293c8604 100644 (file)
@@ -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,,
index 64f3d18e7fc6a45437e7181c0dd8d760fc9838be..d95f1848ad6d862550ced4a7e193ef34e40e697c 100644 (file)
@@ -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
 ----------------------
 
index 7e1b0966fc5e34179b60a062233e851b433874e1..3876d8449afbe20a9434ddf25e76f3bd88599542 100644 (file)
@@ -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 (file)
index 0000000..54c83e0
--- /dev/null
@@ -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)
index b14d500a9c6e9720ee03a13abbbf5ef815ce645e..fa08dc6a25b0eae554b0695ab1e3198cc965f209 100644 (file)
@@ -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 (file)
index 0000000..134d36c
--- /dev/null
@@ -0,0 +1,2 @@
+Add :c:func:`PyType_Freeze` function to make a type immutable. Patch by
+Victor Stinner.
index 62978261745d79127e99682499149cc55211fc3c..f9e51f0683c965f0602b56cd1446737b3d42bcd3 100644 (file)
     added = '3.14'
 [function.PyUnicode_Equal]
     added = '3.14'
+[function.PyType_Freeze]
+    added = '3.14'
index ea26295cca49d4d891324d7b06d18e0b6c1a348e..26f68691e44f831b1bbfcbcb3889b201f6b8395a 100644 (file)
@@ -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 */
 };
 
index 4d843824e1eb00660856cdf3b69c96f884a38b0f..b4a11195613d7443f731e2e9644393ca23379fca 100644 (file)
@@ -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 (; i<PyTuple_GET_SIZE(bases); i++) {
+        PyTypeObject *b = (PyTypeObject*)PyTuple_GET_ITEM(bases, i);
+        if (!b) {
+            return -1;
+        }
+        if (!_PyType_HasFeature(b, Py_TPFLAGS_IMMUTABLETYPE)) {
+            PyErr_Format(
+                PyExc_TypeError,
+                "Creating immutable type %s from mutable base %N",
+                type_name, b
+            );
+            return -1;
+        }
+    }
+    return 0;
+}
+
+
 /* Set *dest to the offset specified by a special "__*offset__" member.
  * Return 0 on success, -1 on failure.
  */
@@ -4820,19 +4846,8 @@ PyType_FromMetaclass(
      * and only heap types can be mutable.)
      */
     if (spec->flags & Py_TPFLAGS_IMMUTABLETYPE) {
-        for (int i=0; i<PyTuple_GET_SIZE(bases); i++) {
-            PyTypeObject *b = (PyTypeObject*)PyTuple_GET_ITEM(bases, i);
-            if (!b) {
-                goto finally;
-            }
-            if (!_PyType_HasFeature(b, Py_TPFLAGS_IMMUTABLETYPE)) {
-                PyErr_Format(
-                    PyExc_TypeError,
-                    "Creating immutable type %s from mutable base %N",
-                    spec->name, 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 {
index 9296474617e115f346871b865c636cd6e8b296a1..8657ddb9fa5155e91eb851627b94cebbe3b01943 100755 (executable)
@@ -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)