diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index bf261b9814456e..c99c7ef93a45df 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -258,7 +258,7 @@ The following functions and structs are used to create (or *Py_tp_base[s]* slots if *bases* is ``NULL``, see below). Metaclasses that override :c:member:`~PyTypeObject.tp_new` are not - supported. + supported, except if ``tp_new`` is ``NULL``. (For backwards compatibility, other ``PyType_From*`` functions allow such metaclasses. They ignore ``tp_new``, which may result in incomplete initialization. This is deprecated and in Python 3.14+ such metaclasses will diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index e1b55cffe8ff52..9cd1554614695e 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -672,31 +672,60 @@ def test_heaptype_with_setattro(self): self.assertEqual(obj.pvalue, 0) def test_heaptype_with_custom_metaclass(self): - self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclass, type)) - self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclassCustomNew, type)) + metaclass = _testcapi.HeapCTypeMetaclass + self.assertTrue(issubclass(metaclass, type)) - t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclass) + # Class creation from C + t = _testcapi.pytype_fromspec_meta(metaclass) self.assertIsInstance(t, type) self.assertEqual(t.__name__, "HeapCTypeViaMetaclass") - self.assertIs(type(t), _testcapi.HeapCTypeMetaclass) + self.assertIs(type(t), metaclass) + + # Class creation from Python + t = metaclass("PyClassViaMetaclass", (), {}) + self.assertIsInstance(t, type) + self.assertEqual(t.__name__, "PyClassViaMetaclass") + + def test_heaptype_with_custom_metaclass_null_new(self): + metaclass = _testcapi.HeapCTypeMetaclassNullNew + + self.assertTrue(issubclass(metaclass, type)) + + # Class creation from C + t = _testcapi.pytype_fromspec_meta(metaclass) + self.assertIsInstance(t, type) + self.assertEqual(t.__name__, "HeapCTypeViaMetaclass") + self.assertIs(type(t), metaclass) + + # Class creation from Python + with self.assertRaisesRegex(TypeError, "cannot create .* instances"): + metaclass("PyClassViaMetaclass", (), {}) + + def test_heaptype_with_custom_metaclass_custom_new(self): + metaclass = _testcapi.HeapCTypeMetaclassCustomNew + + self.assertTrue(issubclass(_testcapi.HeapCTypeMetaclassCustomNew, type)) msg = "Metaclasses with custom tp_new are not supported." with self.assertRaisesRegex(TypeError, msg): - t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclassCustomNew) + t = _testcapi.pytype_fromspec_meta(metaclass) def test_heaptype_with_custom_metaclass_deprecation(self): + metaclass = _testcapi.HeapCTypeMetaclassCustomNew + # gh-103968: a metaclass with custom tp_new is deprecated, but still # allowed for functions that existed in 3.11 # (PyType_FromSpecWithBases is used here). - class Base(metaclass=_testcapi.HeapCTypeMetaclassCustomNew): + class Base(metaclass=metaclass): pass + # Class creation from C with warnings_helper.check_warnings( ('.*custom tp_new.*in Python 3.14.*', DeprecationWarning), ): sub = _testcapi.make_type_with_base(Base) self.assertTrue(issubclass(sub, Base)) - self.assertIsInstance(sub, _testcapi.HeapCTypeMetaclassCustomNew) + self.assertIsInstance(sub, metaclass) def test_multiple_inheritance_ctypes_with_weakref_or_dict(self): diff --git a/Misc/NEWS.d/next/C API/2023-06-06-14-14-41.gh-issue-103968.BTO6II.rst b/Misc/NEWS.d/next/C API/2023-06-06-14-14-41.gh-issue-103968.BTO6II.rst new file mode 100644 index 00000000000000..b73103c4e0ad9e --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-06-06-14-14-41.gh-issue-103968.BTO6II.rst @@ -0,0 +1,2 @@ +:c:func:`PyType_FromMetaclass` now allows metaclasses with ``tp_new`` +set to ``NULL``. diff --git a/Modules/_testcapi/heaptype.c b/Modules/_testcapi/heaptype.c index 565ab570a42bde..c124871e433431 100644 --- a/Modules/_testcapi/heaptype.c +++ b/Modules/_testcapi/heaptype.c @@ -744,6 +744,12 @@ static PyType_Spec HeapCTypeMetaclassCustomNew_spec = { HeapCTypeMetaclassCustomNew_slots }; +static PyType_Spec HeapCTypeMetaclassNullNew_spec = { + .name = "_testcapi.HeapCTypeMetaclassNullNew", + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_DISALLOW_INSTANTIATION, + .slots = empty_type_slots +}; + typedef struct { PyObject_HEAD @@ -1231,6 +1237,13 @@ _PyTestCapi_Init_Heaptype(PyObject *m) { } PyModule_AddObject(m, "HeapCTypeMetaclassCustomNew", HeapCTypeMetaclassCustomNew); + PyObject *HeapCTypeMetaclassNullNew = PyType_FromMetaclass( + &PyType_Type, m, &HeapCTypeMetaclassNullNew_spec, (PyObject *) &PyType_Type); + if (HeapCTypeMetaclassNullNew == NULL) { + return -1; + } + PyModule_AddObject(m, "HeapCTypeMetaclassNullNew", HeapCTypeMetaclassNullNew); + PyObject *HeapCCollection = PyType_FromMetaclass( NULL, m, &HeapCCollection_spec, NULL); if (HeapCCollection == NULL) { diff --git a/Objects/typeobject.c b/Objects/typeobject.c index b6771d3004df91..bf33bde2571012 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4236,7 +4236,7 @@ _PyType_FromMetaclass_impl( metaclass); goto finally; } - if (metaclass->tp_new != PyType_Type.tp_new) { + if (metaclass->tp_new && metaclass->tp_new != PyType_Type.tp_new) { if (_allow_tp_new) { if (PyErr_WarnFormat( PyExc_DeprecationWarning, 1,