Skip to content

Commit 5e34b49

Browse files
authored
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.
1 parent 20d30ba commit 5e34b49

File tree

12 files changed

+150
-14
lines changed

12 files changed

+150
-14
lines changed

Doc/c-api/type.rst

+16-4
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,16 @@ Creating Heap-Allocated Types
190190
The following functions and structs are used to create
191191
:ref:`heap types <heap-types>`.
192192
193-
.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
193+
.. c:function:: PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases)
194194
195-
Creates and returns a :ref:`heap type <heap-types>` from the *spec*
195+
Create and return a :ref:`heap type <heap-types>` from the *spec*
196196
(:const:`Py_TPFLAGS_HEAPTYPE`).
197197
198+
The metaclass *metaclass* is used to construct the resulting type object.
199+
When *metaclass* is ``NULL``, the default :c:type:`PyType_Type` is used
200+
instead. Note that metaclasses that override
201+
:c:member:`~PyTypeObject.tp_new` are not supported.
202+
198203
The *bases* argument can be used to specify base classes; it can either
199204
be only one class or a tuple of classes.
200205
If *bases* is ``NULL``, the *Py_tp_bases* slot is used instead.
@@ -210,22 +215,29 @@ The following functions and structs are used to create
210215
211216
This function calls :c:func:`PyType_Ready` on the new type.
212217
218+
.. versionadded:: 3.12
219+
220+
.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
221+
222+
Equivalent to ``PyType_FromMetaclass(NULL, module, spec, bases)``.
223+
213224
.. versionadded:: 3.9
214225
215226
.. versionchanged:: 3.10
216227
217228
The function now accepts a single class as the *bases* argument and
218229
``NULL`` as the ``tp_doc`` slot.
219230
231+
220232
.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
221233
222-
Equivalent to ``PyType_FromModuleAndSpec(NULL, spec, bases)``.
234+
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, bases)``.
223235
224236
.. versionadded:: 3.3
225237
226238
.. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec)
227239
228-
Equivalent to ``PyType_FromSpecWithBases(spec, NULL)``.
240+
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``.
229241
230242
.. c:type:: PyType_Spec
231243

Doc/c-api/typeobj.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -2071,7 +2071,7 @@ flag set.
20712071

20722072
This is done by filling a :c:type:`PyType_Spec` structure and calling
20732073
:c:func:`PyType_FromSpec`, :c:func:`PyType_FromSpecWithBases`,
2074-
or :c:func:`PyType_FromModuleAndSpec`.
2074+
:c:func:`PyType_FromModuleAndSpec`, or :c:func:`PyType_FromMetaclass`.
20752075

20762076

20772077
.. _number-structs:

Doc/data/stable_abi.dat

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.12.rst

+5
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ C API Changes
151151
New Features
152152
------------
153153

154+
* Added the new limited C API function :c:func:`PyType_FromMetaclass`,
155+
which generalizes the existing :c:func:`PyType_FromModuleAndSpec` using
156+
an additional metaclass argument.
157+
(Contributed by Wenzel Jakob in :gh:`93012`.)
158+
154159
Porting to Python 3.12
155160
----------------------
156161

Include/object.h

+3
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ PyAPI_FUNC(void *) PyType_GetModuleState(PyTypeObject *);
257257
PyAPI_FUNC(PyObject *) PyType_GetName(PyTypeObject *);
258258
PyAPI_FUNC(PyObject *) PyType_GetQualName(PyTypeObject *);
259259
#endif
260+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030C0000
261+
PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spec*, PyObject*);
262+
#endif
260263

261264
/* Generic type check */
262265
PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *);

Lib/test/test_capi.py

+13
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,19 @@ def test_heaptype_with_setattro(self):
605605
del obj.value
606606
self.assertEqual(obj.pvalue, 0)
607607

608+
def test_heaptype_with_custom_metaclass(self):
609+
self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclass, type))
610+
self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclassCustomNew, type))
611+
612+
t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclass)
613+
self.assertIsInstance(t, type)
614+
self.assertEqual(t.__name__, "HeapCTypeViaMetaclass")
615+
self.assertIs(type(t), _testcapi.HeapCTypeMetaclass)
616+
617+
msg = "Metaclasses with custom tp_new are not supported."
618+
with self.assertRaisesRegex(TypeError, msg):
619+
t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclassCustomNew)
620+
608621
def test_pynumber_tobase(self):
609622
from _testcapi import pynumber_tobase
610623
self.assertEqual(pynumber_tobase(123, 2), '0b1111011')

Lib/test/test_stable_abi_ctypes.py

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Added the new function :c:func:`PyType_FromMetaclass`, which generalizes the
2+
existing :c:func:`PyType_FromModuleAndSpec` using an additional metaclass
3+
argument. This is useful for language binding tools, where it can be used to
4+
intercept type-related operations like subclassing or static attribute access
5+
by specifying a metaclass with custom slots.
6+
7+
Importantly, :c:func:`PyType_FromMetaclass` is available in the Limited API,
8+
which provides a path towards migrating more binding tools onto the Stable ABI.

Misc/stable_abi.toml

+2
Original file line numberDiff line numberDiff line change
@@ -2275,3 +2275,5 @@
22752275
added = '3.11'
22762276
[function.PyErr_SetHandledException]
22772277
added = '3.11'
2278+
[function.PyType_FromMetaclass]
2279+
added = '3.12'

Modules/_testcapimodule.c

+73
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,32 @@ test_dict_inner(int count)
308308
}
309309
}
310310

311+
static PyObject *pytype_fromspec_meta(PyObject* self, PyObject *meta)
312+
{
313+
if (!PyType_Check(meta)) {
314+
PyErr_SetString(
315+
TestError,
316+
"pytype_fromspec_meta: must be invoked with a type argument!");
317+
return NULL;
318+
}
319+
320+
PyType_Slot HeapCTypeViaMetaclass_slots[] = {
321+
{0},
322+
};
323+
324+
PyType_Spec HeapCTypeViaMetaclass_spec = {
325+
"_testcapi.HeapCTypeViaMetaclass",
326+
sizeof(PyObject),
327+
0,
328+
Py_TPFLAGS_DEFAULT,
329+
HeapCTypeViaMetaclass_slots
330+
};
331+
332+
return PyType_FromMetaclass(
333+
(PyTypeObject *) meta, NULL, &HeapCTypeViaMetaclass_spec, NULL);
334+
}
335+
336+
311337
static PyObject*
312338
test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored))
313339
{
@@ -5886,6 +5912,7 @@ static PyMethodDef TestMethods[] = {
58865912
{"test_long_numbits", test_long_numbits, METH_NOARGS},
58875913
{"test_k_code", test_k_code, METH_NOARGS},
58885914
{"test_empty_argparse", test_empty_argparse, METH_NOARGS},
5915+
{"pytype_fromspec_meta", pytype_fromspec_meta, METH_O},
58895916
{"parse_tuple_and_keywords", parse_tuple_and_keywords, METH_VARARGS},
58905917
{"pyobject_repr_from_null", pyobject_repr_from_null, METH_NOARGS},
58915918
{"pyobject_str_from_null", pyobject_str_from_null, METH_NOARGS},
@@ -7078,6 +7105,38 @@ static PyType_Spec HeapCTypeSubclassWithFinalizer_spec = {
70787105
HeapCTypeSubclassWithFinalizer_slots
70797106
};
70807107

7108+
static PyType_Slot HeapCTypeMetaclass_slots[] = {
7109+
{0},
7110+
};
7111+
7112+
static PyType_Spec HeapCTypeMetaclass_spec = {
7113+
"_testcapi.HeapCTypeMetaclass",
7114+
sizeof(PyHeapTypeObject),
7115+
sizeof(PyMemberDef),
7116+
Py_TPFLAGS_DEFAULT,
7117+
HeapCTypeMetaclass_slots
7118+
};
7119+
7120+
static PyObject *
7121+
heap_ctype_metaclass_custom_tp_new(PyTypeObject *tp, PyObject *args, PyObject *kwargs)
7122+
{
7123+
return PyType_Type.tp_new(tp, args, kwargs);
7124+
}
7125+
7126+
static PyType_Slot HeapCTypeMetaclassCustomNew_slots[] = {
7127+
{ Py_tp_new, heap_ctype_metaclass_custom_tp_new },
7128+
{0},
7129+
};
7130+
7131+
static PyType_Spec HeapCTypeMetaclassCustomNew_spec = {
7132+
"_testcapi.HeapCTypeMetaclassCustomNew",
7133+
sizeof(PyHeapTypeObject),
7134+
sizeof(PyMemberDef),
7135+
Py_TPFLAGS_DEFAULT,
7136+
HeapCTypeMetaclassCustomNew_slots
7137+
};
7138+
7139+
70817140
typedef struct {
70827141
PyObject_HEAD
70837142
PyObject *dict;
@@ -7591,6 +7650,20 @@ PyInit__testcapi(void)
75917650
Py_DECREF(subclass_with_finalizer_bases);
75927651
PyModule_AddObject(m, "HeapCTypeSubclassWithFinalizer", HeapCTypeSubclassWithFinalizer);
75937652

7653+
PyObject *HeapCTypeMetaclass = PyType_FromMetaclass(
7654+
&PyType_Type, m, &HeapCTypeMetaclass_spec, (PyObject *) &PyType_Type);
7655+
if (HeapCTypeMetaclass == NULL) {
7656+
return NULL;
7657+
}
7658+
PyModule_AddObject(m, "HeapCTypeMetaclass", HeapCTypeMetaclass);
7659+
7660+
PyObject *HeapCTypeMetaclassCustomNew = PyType_FromMetaclass(
7661+
&PyType_Type, m, &HeapCTypeMetaclassCustomNew_spec, (PyObject *) &PyType_Type);
7662+
if (HeapCTypeMetaclassCustomNew == NULL) {
7663+
return NULL;
7664+
}
7665+
PyModule_AddObject(m, "HeapCTypeMetaclassCustomNew", HeapCTypeMetaclassCustomNew);
7666+
75947667
if (PyType_Ready(&ContainerNoGC_type) < 0) {
75957668
return NULL;
75967669
}

Objects/typeobject.c

+26-9
Original file line numberDiff line numberDiff line change
@@ -3366,13 +3366,8 @@ static const PySlot_Offset pyslot_offsets[] = {
33663366
};
33673367

33683368
PyObject *
3369-
PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
3370-
{
3371-
return PyType_FromModuleAndSpec(NULL, spec, bases);
3372-
}
3373-
3374-
PyObject *
3375-
PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
3369+
PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
3370+
PyType_Spec *spec, PyObject *bases)
33763371
{
33773372
PyHeapTypeObject *res;
33783373
PyObject *modname;
@@ -3384,6 +3379,16 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
33843379
char *res_start;
33853380
short slot_offset, subslot_offset;
33863381

3382+
if (!metaclass) {
3383+
metaclass = &PyType_Type;
3384+
}
3385+
3386+
if (metaclass->tp_new != PyType_Type.tp_new) {
3387+
PyErr_SetString(PyExc_TypeError,
3388+
"Metaclasses with custom tp_new are not supported.");
3389+
return NULL;
3390+
}
3391+
33873392
nmembers = weaklistoffset = dictoffset = vectorcalloffset = 0;
33883393
for (slot = spec->slots; slot->slot; slot++) {
33893394
if (slot->slot == Py_tp_members) {
@@ -3412,7 +3417,7 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
34123417
}
34133418
}
34143419

3415-
res = (PyHeapTypeObject*)PyType_GenericAlloc(&PyType_Type, nmembers);
3420+
res = (PyHeapTypeObject*)metaclass->tp_alloc(metaclass, nmembers);
34163421
if (res == NULL)
34173422
return NULL;
34183423
res_start = (char*)res;
@@ -3639,10 +3644,22 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
36393644
return NULL;
36403645
}
36413646

3647+
PyObject *
3648+
PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
3649+
{
3650+
return PyType_FromMetaclass(NULL, module, spec, bases);
3651+
}
3652+
3653+
PyObject *
3654+
PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
3655+
{
3656+
return PyType_FromMetaclass(NULL, NULL, spec, bases);
3657+
}
3658+
36423659
PyObject *
36433660
PyType_FromSpec(PyType_Spec *spec)
36443661
{
3645-
return PyType_FromSpecWithBases(spec, NULL);
3662+
return PyType_FromMetaclass(NULL, NULL, spec, NULL);
36463663
}
36473664

36483665
PyObject *

PC/python3dll.c

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)