Skip to content

Commit

Permalink
gh-121654: Add PyType_Freeze() function (#122457)
Browse files Browse the repository at this point in the history
Co-authored-by: Petr Viktorin <[email protected]>
  • Loading branch information
vstinner and encukou authored Oct 25, 2024
1 parent da8673d commit db96327
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 13 deletions.
14 changes: 14 additions & 0 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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) -->
Expand Down
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------------

Expand Down
4 changes: 4 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_capi/test_type.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyType_Freeze` function to make a type immutable. Patch by
Victor Stinner.
2 changes: 2 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2538,3 +2538,5 @@
added = '3.14'
[function.PyUnicode_Equal]
added = '3.14'
[function.PyType_Freeze]
added = '3.14'
16 changes: 16 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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 */
};

Expand Down
65 changes: 52 additions & 13 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions PC/python3dll.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit db96327

Please sign in to comment.