https://github.com/python/cpython/commit/db96327203b09ada45f2214567f92fe4d837f82a
commit: db96327203b09ada45f2214567f92fe4d837f82a
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2024-10-25T11:12:48+02:00
summary:

gh-121654: Add PyType_Freeze() function (#122457)

Co-authored-by: Petr Viktorin <[email protected]>

files:
A Lib/test/test_capi/test_type.py
A Misc/NEWS.d/next/C_API/2024-07-30-14-40-08.gh-issue-121654.tgGeAl.rst
M Doc/c-api/type.rst
M Doc/data/stable_abi.dat
M Doc/whatsnew/3.14.rst
M Include/object.h
M Lib/test/test_stable_abi_ctypes.py
M Misc/stable_abi.toml
M Modules/_testcapimodule.c
M Objects/typeobject.c
M PC/python3dll.c

diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 0031708c4680cc..86d3967d9fb577 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
 
    <!-- Keep old URL fragments working (see gh-97908) -->
diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat
index 9314facd2ad873..6f9d27297e8f65 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 64f3d18e7fc6a4..d95f1848ad6d86 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 7e1b0966fc5e34..3876d8449afbe2 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 00000000000000..54c83e09f892a0
--- /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 b14d500a9c6e97..fa08dc6a25b0ea 100644
--- a/Lib/test/test_stable_abi_ctypes.py
+++ b/Lib/test/test_stable_abi_ctypes.py
@@ -713,6 +713,7 @@ def test_windows_feature_macros(self):
     "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 00000000000000..134d36c281ab21
--- /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 62978261745d79..f9e51f0683c965 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 ea26295cca49d4..26f68691e44f83 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 4d843824e1eb00..b4a11195613d74 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 (; 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 {
diff --git a/PC/python3dll.c b/PC/python3dll.c
index 9296474617e115..8657ddb9fa5155 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)

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to