[Python-checkins] gh-60074: add new stable API function PyType_FromMetaclass (GH-93012)

encukou webhook-mailer at python.org
Fri May 27 04:27:55 EDT 2022


https://github.com/python/cpython/commit/5e34b494a08015e9b5a3deade23943bdba284a93
commit: 5e34b494a08015e9b5a3deade23943bdba284a93
branch: main
author: Wenzel Jakob <wenzel.jakob at epfl.ch>
committer: encukou <encukou at gmail.com>
date: 2022-05-27T10:27:39+02:00
summary:

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.

files:
A Misc/NEWS.d/next/Core and Builtins/2022-05-20-13-32-24.gh-issue-93012.e9B-pv.rst
M Doc/c-api/type.rst
M Doc/c-api/typeobj.rst
M Doc/data/stable_abi.dat
M Doc/whatsnew/3.12.rst
M Include/object.h
M Lib/test/test_capi.py
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 d740e4eb0897e..99b3845237d86 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 <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 <heap-types>` from the *spec*
+   Create and return a :ref:`heap type <heap-types>` 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 b3f371bb9c062..df479046d4aeb 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 3912a7c1242de..82cd5796efd27 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 033de1780b3d1..fd487848f09a3 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 f01b9fa86d014..a3c6bd4fa984d 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 49cd82108c92c..95930ba40ceb5 100644
--- a/Lib/test/test_capi.py
+++ b/Lib/test/test_capi.py
@@ -605,6 +605,19 @@ def test_heaptype_with_setattro(self):
         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 18c85061ca089..53e93ab6b9b4c 100644
--- a/Lib/test/test_stable_abi_ctypes.py
+++ b/Lib/test/test_stable_abi_ctypes.py
@@ -660,6 +660,7 @@ def test_windows_feature_macros(self):
     "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 0000000000000..8de0f000647dc
--- /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 d848f18d68ff6..84bec82709605 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 3bc776140aaba..37f4ded8001c6 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 1daf2b8d3b0ff..ff5196c904eef 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 50e7a9607bec9..024ec49d68d79 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)



More information about the Python-checkins mailing list