From 444a17c205599f72281e65a15bcafa1451f150a5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 30 Sep 2024 17:23:27 +0200 Subject: [PATCH 1/2] gh-121654: Add Py_tp_create_callback slot --- Doc/c-api/type.rst | 20 ++++++++++++++++++- Doc/whatsnew/3.14.rst | 5 +++++ Include/typeslots.h | 4 ++++ Lib/test/test_capi/test_misc.py | 9 +++++++++ Misc/stable_abi.toml | 2 ++ Modules/_testcapi/heaptype.c | 28 +++++++++++++++++++++++++++ Objects/typeobject.c | 34 ++++++++++++++++++++++++++------- Objects/typeslots.inc | 1 + Objects/typeslots.py | 3 +++ 9 files changed, 98 insertions(+), 8 deletions(-) diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 0031708c4680cc..6c1b63bb62cbe9 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -508,10 +508,11 @@ The following functions and structs are used to create * ``Py_nb_add`` to set :c:member:`PyNumberMethods.nb_add` * ``Py_sq_length`` to set :c:member:`PySequenceMethods.sq_length` - An additional slot is supported that does not correspond to a + Additional slots are supported that don't correspond to a :c:type:`!PyTypeObject` struct field: * :c:data:`Py_tp_token` + * :c:data:`Py_tp_create_callback` The following “offset” fields cannot be set using :c:type:`PyType_Slot`: @@ -607,3 +608,20 @@ The following functions and structs are used to create Expands to ``NULL``. .. versionadded:: 3.14 + + +.. c:macro:: Py_tp_create_callback + + A :c:member:`~PyType_Slot.slot` that records a callback to create a type. + + Prototype:: + + int create_callback(PyTypeObject *type) + + The callback must return ``0`` on success, or set an exception and return + ``-1`` on error. + + For example, the callback can be used to customize a type (set attributes) + before it's made immutable by the :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. + + .. versionadded:: 3.14 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ffc001241ac5ec..6f0105bc624c3f 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -680,6 +680,11 @@ New Features `__ mentioned in :pep:`630` (:gh:`124153`). +* Add :c:macro:`Py_tp_create_callback` slot to customize a class (set + attributes) before it's made immutable by the + :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. + (Contributed by Victor Stinner in :gh:`121654`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/typeslots.h b/Include/typeslots.h index a7f3017ec02e92..8ec9ae19041a09 100644 --- a/Include/typeslots.h +++ b/Include/typeslots.h @@ -94,3 +94,7 @@ /* New in 3.14 */ #define Py_tp_token 83 #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000 +/* New in 3.14 */ +#define Py_tp_create_callback 84 +#endif diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 5c6faa1626d380..1853ee57668892 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -1221,6 +1221,15 @@ def genf(): yield gen = genf() self.assertEqual(_testcapi.gen_get_code(gen), gen.gi_code) + def test_immutable_type(self): + immutable = _testcapi.Immutable + + # Attribute created by the 'Py_tp_create_callback' callback + self.assertEqual(immutable.attr, "value") + + with self.assertRaisesRegex(TypeError, "cannot set .* immutable type"): + setattr(immutable, "attr2", "value2") + @requires_limited_api class TestHeapTypeRelative(unittest.TestCase): diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index fe0a5e44f8fb15..b7c69a7b297dfe 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2536,3 +2536,5 @@ added = '3.14' [const.Py_TP_USE_SPEC] added = '3.14' +[const.Py_tp_create_callback] + added = '3.14' diff --git a/Modules/_testcapi/heaptype.c b/Modules/_testcapi/heaptype.c index cc88147dfcd7fb..ea64c9cd6988a6 100644 --- a/Modules/_testcapi/heaptype.c +++ b/Modules/_testcapi/heaptype.c @@ -1313,6 +1313,31 @@ static PyType_Spec HeapCCollection_spec = { .slots = HeapCCollection_slots, }; +static int +immutable_create_callback(PyTypeObject *type) +{ + PyObject *value = PyUnicode_FromString("value"); + if (value == NULL) { + return -1; + } + + int res = PyObject_SetAttrString((PyObject*)type, "attr", value); + Py_DECREF(value); + return res; +} + +static PyType_Slot Immutable_slots[] = { + {Py_tp_create_callback, (void *)immutable_create_callback}, + {0, 0}, +}; + +static PyType_Spec Immutable_spec = { + .name = "_testcapi.Immutable", + .basicsize = sizeof(PyObject), + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE), + .slots = Immutable_slots, +}; + int _PyTestCapi_Init_Heaptype(PyObject *m) { _testcapimodule = PyModule_GetDef(m); @@ -1415,5 +1440,8 @@ _PyTestCapi_Init_Heaptype(PyObject *m) { return -1; } + PyObject *Immutable = PyType_FromSpec(&Immutable_spec); + ADD("Immutable", Immutable); + return 0; } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 0e2d9758a5ffae..25551962ca69dc 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4926,8 +4926,9 @@ PyType_FromMetaclass( res_start = (char*)res; type = &res->ht_type; - /* The flags must be initialized early, before the GC traverses us */ - type->tp_flags = spec->flags | Py_TPFLAGS_HEAPTYPE; + /* The flags must be initialized early, before the GC traverses us. + * Py_TPFLAGS_IMMUTABLETYPE flag is set at the end. */ + type->tp_flags = (spec->flags & ~Py_TPFLAGS_IMMUTABLETYPE) | Py_TPFLAGS_HEAPTYPE; res->ht_module = Py_XNewRef(module); @@ -4963,6 +4964,7 @@ PyType_FromMetaclass( /* Copy all the ordinary slots */ + void *create_callback = NULL; for (slot = spec->slots; slot->slot; slot++) { switch (slot->slot) { case Py_tp_base: @@ -4993,6 +4995,11 @@ PyType_FromMetaclass( res->ht_token = slot->pfunc == Py_TP_USE_SPEC ? spec : slot->pfunc; } break; + case Py_tp_create_callback: + { + create_callback = slot->pfunc; + } + break; default: { /* Copy other slots directly */ @@ -5094,6 +5101,17 @@ PyType_FromMetaclass( } } + if (create_callback != NULL) { + int (*callback) (PyTypeObject*) = create_callback; + if (callback(type) < 0) { + assert(PyErr_Occurred()); + goto finally; + } + } + if (spec->flags & Py_TPFLAGS_IMMUTABLETYPE) { + type->tp_flags |= Py_TPFLAGS_IMMUTABLETYPE; + } + assert(_PyType_CheckConsistency(type)); finally: @@ -5146,14 +5164,16 @@ PyType_GetModuleName(PyTypeObject *type) void * PyType_GetSlot(PyTypeObject *type, int slot) { - void *parent_slot; - int slots_len = Py_ARRAY_LENGTH(pyslot_offsets); - - if (slot <= 0 || slot >= slots_len) { + size_t slots_len = Py_ARRAY_LENGTH(pyslot_offsets); + if (slot <= 0 || (size_t)slot >= slots_len) { PyErr_BadInternalCall(); return NULL; } int slot_offset = pyslot_offsets[slot].slot_offset; + if (slot_offset == -1) { + // Special case for Py_tp_create_callback: always NULL + return NULL; + } if (slot_offset >= (int)sizeof(PyTypeObject)) { if (!_PyType_HasFeature(type, Py_TPFLAGS_HEAPTYPE)) { @@ -5161,7 +5181,7 @@ PyType_GetSlot(PyTypeObject *type, int slot) } } - parent_slot = *(void**)((char*)type + slot_offset); + void *parent_slot = *(void**)((char*)type + slot_offset); if (parent_slot == NULL) { return NULL; } diff --git a/Objects/typeslots.inc b/Objects/typeslots.inc index 642160fe0bd8bc..599bb0564825ab 100644 --- a/Objects/typeslots.inc +++ b/Objects/typeslots.inc @@ -82,3 +82,4 @@ {offsetof(PyAsyncMethods, am_send), offsetof(PyTypeObject, tp_as_async)}, {-1, offsetof(PyTypeObject, tp_vectorcall)}, {-1, offsetof(PyHeapTypeObject, ht_token)}, +{-1, -1}, diff --git a/Objects/typeslots.py b/Objects/typeslots.py index c7f8a33bb1e74e..6007cc53f89530 100755 --- a/Objects/typeslots.py +++ b/Objects/typeslots.py @@ -17,6 +17,9 @@ def generate_typeslots(out=sys.stdout): # The heap type structure (ht_*) is an implementation detail; # the public slot for it has a familiar `tp_` prefix member = '{-1, offsetof(PyHeapTypeObject, ht_token)}' + elif member == "tp_create_callback": + # PyType_GetSlot(tp_create_callback) returns NULL + member = '{-1, -1}' elif member.startswith("tp_"): member = f'{{-1, offsetof(PyTypeObject, {member})}}' elif member.startswith("am_"): From e294f84576d9eb2f63f215aa63cc90081ac463dd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 30 Sep 2024 17:47:10 +0200 Subject: [PATCH 2/2] Add NEWS entry --- Doc/c-api/type.rst | 5 +++-- Doc/whatsnew/3.14.rst | 2 +- .../C_API/2024-09-30-17-47-09.gh-issue-121654.nOpuXO.rst | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2024-09-30-17-47-09.gh-issue-121654.nOpuXO.rst diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 6c1b63bb62cbe9..cef090c4dbf3b1 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -621,7 +621,8 @@ The following functions and structs are used to create The callback must return ``0`` on success, or set an exception and return ``-1`` on error. - For example, the callback can be used to customize a type (set attributes) - before it's made immutable by the :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. + For example, the callback can be used to customize a type (ex: set + attributes) before it's made immutable by the + :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. .. versionadded:: 3.14 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 6f0105bc624c3f..052275099ae4aa 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -680,7 +680,7 @@ New Features `__ mentioned in :pep:`630` (:gh:`124153`). -* Add :c:macro:`Py_tp_create_callback` slot to customize a class (set +* Add :c:macro:`Py_tp_create_callback` slot to customize a class (ex: set attributes) before it's made immutable by the :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. (Contributed by Victor Stinner in :gh:`121654`.) diff --git a/Misc/NEWS.d/next/C_API/2024-09-30-17-47-09.gh-issue-121654.nOpuXO.rst b/Misc/NEWS.d/next/C_API/2024-09-30-17-47-09.gh-issue-121654.nOpuXO.rst new file mode 100644 index 00000000000000..8a77b72eb7b862 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-09-30-17-47-09.gh-issue-121654.nOpuXO.rst @@ -0,0 +1,3 @@ +Add :c:macro:`Py_tp_create_callback` slot to customize a class (ex: set +attributes) before it's made immutable by the +:c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag. Patch by Victor Stinner.