From 5e34b494a08015e9b5a3deade23943bdba284a93 Mon Sep 17 00:00:00 2001 From: Wenzel Jakob Date: Fri, 27 May 2022 10:27:39 +0200 Subject: [PATCH] gh-60074: add new stable API function PyType_FromMetaclass (GH-93012) Added a new stable API function ``PyType_FromMetaclass``, which mirrors the behavior of ``PyType_FromModuleAndSpec`` except that it takes an additional metaclass argument. This is, e.g., useful for language binding tools that need to store additional information in the type object. --- Doc/c-api/type.rst | 20 ++++- Doc/c-api/typeobj.rst | 2 +- Doc/data/stable_abi.dat | 1 + Doc/whatsnew/3.12.rst | 5 ++ Include/object.h | 3 + Lib/test/test_capi.py | 13 ++++ Lib/test/test_stable_abi_ctypes.py | 1 + ...2-05-20-13-32-24.gh-issue-93012.e9B-pv.rst | 8 ++ Misc/stable_abi.toml | 2 + Modules/_testcapimodule.c | 73 +++++++++++++++++++ Objects/typeobject.c | 35 ++++++--- PC/python3dll.c | 1 + 12 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-20-13-32-24.gh-issue-93012.e9B-pv.rst diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index d740e4eb089..99b3845237d 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -190,11 +190,16 @@ Creating Heap-Allocated Types The following functions and structs are used to create :ref:`heap types `. -.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) +.. c:function:: PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases) - Creates and returns a :ref:`heap type ` from the *spec* + Create and return a :ref:`heap type ` from the *spec* (:const:`Py_TPFLAGS_HEAPTYPE`). + The metaclass *metaclass* is used to construct the resulting type object. + When *metaclass* is ``NULL``, the default :c:type:`PyType_Type` is used + instead. Note that metaclasses that override + :c:member:`~PyTypeObject.tp_new` are not supported. + The *bases* argument can be used to specify base classes; it can either be only one class or a tuple of classes. If *bases* is ``NULL``, the *Py_tp_bases* slot is used instead. @@ -210,6 +215,12 @@ The following functions and structs are used to create This function calls :c:func:`PyType_Ready` on the new type. + .. versionadded:: 3.12 + +.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) + + Equivalent to ``PyType_FromMetaclass(NULL, module, spec, bases)``. + .. versionadded:: 3.9 .. versionchanged:: 3.10 @@ -217,15 +228,16 @@ The following functions and structs are used to create The function now accepts a single class as the *bases* argument and ``NULL`` as the ``tp_doc`` slot. + .. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) - Equivalent to ``PyType_FromModuleAndSpec(NULL, spec, bases)``. + Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, bases)``. .. versionadded:: 3.3 .. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec) - Equivalent to ``PyType_FromSpecWithBases(spec, NULL)``. + Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``. .. c:type:: PyType_Spec diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index b3f371bb9c0..df479046d4a 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -2071,7 +2071,7 @@ flag set. This is done by filling a :c:type:`PyType_Spec` structure and calling :c:func:`PyType_FromSpec`, :c:func:`PyType_FromSpecWithBases`, -or :c:func:`PyType_FromModuleAndSpec`. +:c:func:`PyType_FromModuleAndSpec`, or :c:func:`PyType_FromMetaclass`. .. _number-structs: diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 3912a7c1242..82cd5796efd 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -653,6 +653,7 @@ function,PyTuple_Size,3.2,, var,PyTuple_Type,3.2,, type,PyTypeObject,3.2,,opaque function,PyType_ClearCache,3.2,, +function,PyType_FromMetaclass,3.12,, function,PyType_FromModuleAndSpec,3.10,, function,PyType_FromSpec,3.2,, function,PyType_FromSpecWithBases,3.3,, diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 033de1780b3..fd487848f09 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -151,6 +151,11 @@ C API Changes New Features ------------ +* Added the new limited C API function :c:func:`PyType_FromMetaclass`, + which generalizes the existing :c:func:`PyType_FromModuleAndSpec` using + an additional metaclass argument. + (Contributed by Wenzel Jakob in :gh:`93012`.) + Porting to Python 3.12 ---------------------- diff --git a/Include/object.h b/Include/object.h index f01b9fa86d0..a3c6bd4fa98 100644 --- a/Include/object.h +++ b/Include/object.h @@ -257,6 +257,9 @@ PyAPI_FUNC(void *) PyType_GetModuleState(PyTypeObject *); PyAPI_FUNC(PyObject *) PyType_GetName(PyTypeObject *); PyAPI_FUNC(PyObject *) PyType_GetQualName(PyTypeObject *); #endif +#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030C0000 +PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spec*, PyObject*); +#endif /* Generic type check */ PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *); diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 49cd82108c9..95930ba40ce 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -605,6 +605,19 @@ class CAPITest(unittest.TestCase): del obj.value self.assertEqual(obj.pvalue, 0) + def test_heaptype_with_custom_metaclass(self): + self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclass, type)) + self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclassCustomNew, type)) + + t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclass) + self.assertIsInstance(t, type) + self.assertEqual(t.__name__, "HeapCTypeViaMetaclass") + self.assertIs(type(t), _testcapi.HeapCTypeMetaclass) + + msg = "Metaclasses with custom tp_new are not supported." + with self.assertRaisesRegex(TypeError, msg): + t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclassCustomNew) + def test_pynumber_tobase(self): from _testcapi import pynumber_tobase self.assertEqual(pynumber_tobase(123, 2), '0b1111011') diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 18c85061ca0..53e93ab6b9b 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -660,6 +660,7 @@ SYMBOL_NAMES = ( "PyTuple_Size", "PyTuple_Type", "PyType_ClearCache", + "PyType_FromMetaclass", "PyType_FromModuleAndSpec", "PyType_FromSpec", "PyType_FromSpecWithBases", diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-20-13-32-24.gh-issue-93012.e9B-pv.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-20-13-32-24.gh-issue-93012.e9B-pv.rst new file mode 100644 index 00000000000..8de0f000647 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-20-13-32-24.gh-issue-93012.e9B-pv.rst @@ -0,0 +1,8 @@ +Added the new function :c:func:`PyType_FromMetaclass`, which generalizes the +existing :c:func:`PyType_FromModuleAndSpec` using an additional metaclass +argument. This is useful for language binding tools, where it can be used to +intercept type-related operations like subclassing or static attribute access +by specifying a metaclass with custom slots. + +Importantly, :c:func:`PyType_FromMetaclass` is available in the Limited API, +which provides a path towards migrating more binding tools onto the Stable ABI. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index d848f18d68f..84bec827096 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -2275,3 +2275,5 @@ added = '3.11' [function.PyErr_SetHandledException] added = '3.11' +[function.PyType_FromMetaclass] + added = '3.12' diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 3bc776140aa..37f4ded8001 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -308,6 +308,32 @@ test_dict_inner(int count) } } +static PyObject *pytype_fromspec_meta(PyObject* self, PyObject *meta) +{ + if (!PyType_Check(meta)) { + PyErr_SetString( + TestError, + "pytype_fromspec_meta: must be invoked with a type argument!"); + return NULL; + } + + PyType_Slot HeapCTypeViaMetaclass_slots[] = { + {0}, + }; + + PyType_Spec HeapCTypeViaMetaclass_spec = { + "_testcapi.HeapCTypeViaMetaclass", + sizeof(PyObject), + 0, + Py_TPFLAGS_DEFAULT, + HeapCTypeViaMetaclass_slots + }; + + return PyType_FromMetaclass( + (PyTypeObject *) meta, NULL, &HeapCTypeViaMetaclass_spec, NULL); +} + + static PyObject* test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored)) { @@ -5886,6 +5912,7 @@ static PyMethodDef TestMethods[] = { {"test_long_numbits", test_long_numbits, METH_NOARGS}, {"test_k_code", test_k_code, METH_NOARGS}, {"test_empty_argparse", test_empty_argparse, METH_NOARGS}, + {"pytype_fromspec_meta", pytype_fromspec_meta, METH_O}, {"parse_tuple_and_keywords", parse_tuple_and_keywords, METH_VARARGS}, {"pyobject_repr_from_null", pyobject_repr_from_null, METH_NOARGS}, {"pyobject_str_from_null", pyobject_str_from_null, METH_NOARGS}, @@ -7078,6 +7105,38 @@ static PyType_Spec HeapCTypeSubclassWithFinalizer_spec = { HeapCTypeSubclassWithFinalizer_slots }; +static PyType_Slot HeapCTypeMetaclass_slots[] = { + {0}, +}; + +static PyType_Spec HeapCTypeMetaclass_spec = { + "_testcapi.HeapCTypeMetaclass", + sizeof(PyHeapTypeObject), + sizeof(PyMemberDef), + Py_TPFLAGS_DEFAULT, + HeapCTypeMetaclass_slots +}; + +static PyObject * +heap_ctype_metaclass_custom_tp_new(PyTypeObject *tp, PyObject *args, PyObject *kwargs) +{ + return PyType_Type.tp_new(tp, args, kwargs); +} + +static PyType_Slot HeapCTypeMetaclassCustomNew_slots[] = { + { Py_tp_new, heap_ctype_metaclass_custom_tp_new }, + {0}, +}; + +static PyType_Spec HeapCTypeMetaclassCustomNew_spec = { + "_testcapi.HeapCTypeMetaclassCustomNew", + sizeof(PyHeapTypeObject), + sizeof(PyMemberDef), + Py_TPFLAGS_DEFAULT, + HeapCTypeMetaclassCustomNew_slots +}; + + typedef struct { PyObject_HEAD PyObject *dict; @@ -7591,6 +7650,20 @@ PyInit__testcapi(void) Py_DECREF(subclass_with_finalizer_bases); PyModule_AddObject(m, "HeapCTypeSubclassWithFinalizer", HeapCTypeSubclassWithFinalizer); + PyObject *HeapCTypeMetaclass = PyType_FromMetaclass( + &PyType_Type, m, &HeapCTypeMetaclass_spec, (PyObject *) &PyType_Type); + if (HeapCTypeMetaclass == NULL) { + return NULL; + } + PyModule_AddObject(m, "HeapCTypeMetaclass", HeapCTypeMetaclass); + + PyObject *HeapCTypeMetaclassCustomNew = PyType_FromMetaclass( + &PyType_Type, m, &HeapCTypeMetaclassCustomNew_spec, (PyObject *) &PyType_Type); + if (HeapCTypeMetaclassCustomNew == NULL) { + return NULL; + } + PyModule_AddObject(m, "HeapCTypeMetaclassCustomNew", HeapCTypeMetaclassCustomNew); + if (PyType_Ready(&ContainerNoGC_type) < 0) { return NULL; } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1daf2b8d3b0..ff5196c904e 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3366,13 +3366,8 @@ static const PySlot_Offset pyslot_offsets[] = { }; PyObject * -PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) -{ - return PyType_FromModuleAndSpec(NULL, spec, bases); -} - -PyObject * -PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) +PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, + PyType_Spec *spec, PyObject *bases) { PyHeapTypeObject *res; PyObject *modname; @@ -3384,6 +3379,16 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) char *res_start; short slot_offset, subslot_offset; + if (!metaclass) { + metaclass = &PyType_Type; + } + + if (metaclass->tp_new != PyType_Type.tp_new) { + PyErr_SetString(PyExc_TypeError, + "Metaclasses with custom tp_new are not supported."); + return NULL; + } + nmembers = weaklistoffset = dictoffset = vectorcalloffset = 0; for (slot = spec->slots; slot->slot; slot++) { if (slot->slot == Py_tp_members) { @@ -3412,7 +3417,7 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) } } - res = (PyHeapTypeObject*)PyType_GenericAlloc(&PyType_Type, nmembers); + res = (PyHeapTypeObject*)metaclass->tp_alloc(metaclass, nmembers); if (res == NULL) return NULL; res_start = (char*)res; @@ -3639,10 +3644,22 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) return NULL; } +PyObject * +PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases) +{ + return PyType_FromMetaclass(NULL, module, spec, bases); +} + +PyObject * +PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) +{ + return PyType_FromMetaclass(NULL, NULL, spec, bases); +} + PyObject * PyType_FromSpec(PyType_Spec *spec) { - return PyType_FromSpecWithBases(spec, NULL); + return PyType_FromMetaclass(NULL, NULL, spec, NULL); } PyObject * diff --git a/PC/python3dll.c b/PC/python3dll.c index 50e7a9607be..024ec49d68d 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -599,6 +599,7 @@ EXPORT_FUNC(PyTuple_Pack) EXPORT_FUNC(PyTuple_SetItem) EXPORT_FUNC(PyTuple_Size) EXPORT_FUNC(PyType_ClearCache) +EXPORT_FUNC(PyType_FromMetaclass) EXPORT_FUNC(PyType_FromModuleAndSpec) EXPORT_FUNC(PyType_FromSpec) EXPORT_FUNC(PyType_FromSpecWithBases)