Skip to content

Commit 05940a8

Browse files
committed
pythongh-60074: add new stable API function PyType_FromMetaclass
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 d853758 commit 05940a8

File tree

12 files changed

+135
-4
lines changed

12 files changed

+135
-4
lines changed

Doc/c-api/type.rst

+15-2
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ The following functions and structs are used to create
192192
193193
.. c:function:: PyObject* PyType_FromModuleAndSpec(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
198198
The *bases* argument can be used to specify base classes; it can either
@@ -208,7 +208,9 @@ The following functions and structs are used to create
208208
The associated module is not inherited by subclasses; it must be specified
209209
for each class individually.
210210
211-
This function calls :c:func:`PyType_Ready` on the new type.
211+
This function calls :c:func:`PyType_Ready` on the new type. Its behavior is
212+
equivalent to ``PyType_FromMetaclass(&PyType_Type, NULL, spec,
213+
bases)``.
212214
213215
.. versionadded:: 3.9
214216
@@ -217,6 +219,17 @@ The following functions and structs are used to create
217219
The function now accepts a single class as the *bases* argument and
218220
``NULL`` as the ``tp_doc`` slot.
219221
222+
.. c:function:: PyObject* PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module, PyType_Spec *spec, PyObject *bases)
223+
224+
Create and return a :ref:`heap type <heap-types>` from the *spec*
225+
(:const:`Py_TPFLAGS_HEAPTYPE`). This function is a generalization of
226+
:c:func:`PyType_FromModuleAndSpec`, with the main difference being that
227+
the metaclass *metaclass* is used to construct the resulting type object.
228+
229+
Metaclasses that override :c:member:`~PyTypeObject.tp_new` are not supported.
230+
231+
.. versionadded:: 3.11
232+
220233
.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
221234
222235
Equivalent to ``PyType_FromModuleAndSpec(NULL, spec, bases)``.

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.11.rst

+5
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,11 @@ New Features
16081608
* Added the :c:member:`PyConfig.safe_path` member.
16091609
(Contributed by Victor Stinner in :gh:`57684`.)
16101610

1611+
* Added the new limited C API function :c:func:`PyType_FromMetaclass`,
1612+
which generalizes the existing :c:func:`PyType_FromModuleAndSpec` using
1613+
an additional metaclass argument.
1614+
(Contributed by Wenzel Jakob in :gh:`93012`.)
1615+
16111616
Porting to Python 3.11
16121617
----------------------
16131618

Include/object.h

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ PyAPI_FUNC(void *) PyType_GetModuleState(PyTypeObject *);
256256
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030B0000
257257
PyAPI_FUNC(PyObject *) PyType_GetName(PyTypeObject *);
258258
PyAPI_FUNC(PyObject *) PyType_GetQualName(PyTypeObject *);
259+
PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spec*, PyObject*);
259260
#endif
260261

261262
/* Generic type check */

Lib/test/test_capi.py

+13
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,19 @@ def test_heaptype_with_setattro(self):
608608
del obj.value
609609
self.assertEqual(obj.pvalue, 0)
610610

611+
def test_heaptype_with_custom_metaclass(self):
612+
self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclass, type))
613+
self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclassCustomNew, type))
614+
615+
t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclass)
616+
self.assertIsInstance(t, type)
617+
self.assertEqual(t.__name__, "HeapCTypeViaMetaclass")
618+
self.assertIs(type(t), _testcapi.HeapCTypeMetaclass)
619+
620+
msg = "Metaclasses with custom tp_new are not supported."
621+
with self.assertRaisesRegex(TypeError, msg):
622+
t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclassCustomNew)
623+
611624
def test_pynumber_tobase(self):
612625
from _testcapi import pynumber_tobase
613626
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.11'

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -3373,6 +3373,13 @@ PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
33733373

33743374
PyObject *
33753375
PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
3376+
{
3377+
return PyType_FromMetaclass(&PyType_Type, module, spec, bases);
3378+
}
3379+
3380+
PyObject *
3381+
PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
3382+
PyType_Spec *spec, PyObject *bases)
33763383
{
33773384
PyHeapTypeObject *res;
33783385
PyObject *modname;
@@ -3384,6 +3391,12 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
33843391
char *res_start;
33853392
short slot_offset, subslot_offset;
33863393

3394+
if (metaclass->tp_new != PyType_Type.tp_new) {
3395+
PyErr_SetString(PyExc_TypeError,
3396+
"Metaclasses with custom tp_new are not supported.");
3397+
return NULL;
3398+
}
3399+
33873400
nmembers = weaklistoffset = dictoffset = vectorcalloffset = 0;
33883401
for (slot = spec->slots; slot->slot; slot++) {
33893402
if (slot->slot == Py_tp_members) {
@@ -3412,7 +3425,7 @@ PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
34123425
}
34133426
}
34143427

3415-
res = (PyHeapTypeObject*)PyType_GenericAlloc(&PyType_Type, nmembers);
3428+
res = (PyHeapTypeObject*)metaclass->tp_alloc(metaclass, nmembers);
34163429
if (res == NULL)
34173430
return NULL;
34183431
res_start = (char*)res;

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)