Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-103968: Deprecate creating heap types whose metaclass has custom tp_new. #103972

Merged
merged 4 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,13 @@ The following functions and structs are used to create
The metaclass *metaclass* is used to construct the resulting type object.
When *metaclass* is ``NULL``, the metaclass is derived from *bases*
(or *Py_tp_base[s]* slots if *bases* is ``NULL``, see below).
Note that metaclasses that override
:c:member:`~PyTypeObject.tp_new` are not supported.

Metaclasses that override :c:member:`~PyTypeObject.tp_new` are not
supported.
(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
not be supported.)
Comment on lines +262 to +265
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be nested in a .. deprecated:: 3.12 block I suppose? And below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mentioned in .. versionchanged:: 3.12 in the affected functions.
Their docs are just Equivalent to ``PyType_FromMetaclass(NULL, NULL, arg, NULL, NULL)`` and a bunch of change notes, so the PyType_FromMetaclass docs need this parenthetical to be complete.


The *bases* argument can be used to specify base classes; it can either
be only one class or a tuple of classes.
Expand Down Expand Up @@ -305,6 +310,11 @@ The following functions and structs are used to create
The function now finds and uses a metaclass corresponding to the provided
base classes. Previously, only :class:`type` instances were returned.

The :c:member:`~PyTypeObject.tp_new` of the metaclass is *ignored*.
which may result in incomplete initialization.
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is deprecated and in Python 3.14+ it
will be no longer allowed.

.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)

Expand All @@ -317,6 +327,12 @@ The following functions and structs are used to create
The function now finds and uses a metaclass corresponding to the provided
base classes. Previously, only :class:`type` instances were returned.

The :c:member:`~PyTypeObject.tp_new` of the metaclass is *ignored*.
which may result in incomplete initialization.
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is deprecated and in Python 3.14+ it
will be no longer allowed.

.. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec)

Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``.
Expand All @@ -327,6 +343,12 @@ The following functions and structs are used to create
base classes provided in *Py_tp_base[s]* slots.
Previously, only :class:`type` instances were returned.

The :c:member:`~PyTypeObject.tp_new` of the metaclass is *ignored*.
which may result in incomplete initialization.
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is deprecated and in Python 3.14+ it
will be no longer allowed.

.. c:type:: PyType_Spec

Structure defining a type's behavior.
Expand Down
20 changes: 20 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,21 @@ Porting to Python 3.12
available on debug builds. If you happen to be using it then you'll
need to start using ``_Py_GetGlobalRefTotal()``.

* The following functions now select an appropriate metaclass for the newly
created type:

* :c:func:`PyType_FromSpec`
* :c:func:`PyType_FromSpecWithBases`
* :c:func:`PyType_FromModuleAndSpec`

Creating classes whose metaclass overrides :c:member:`~PyTypeObject.tp_new`
is deprecated, and in Python 3.14+ it will be disallowed.
Note that these functions ignore ``tp_new`` of the metaclass, possibly
allowing incomplete initialization.

Note that :c:func:`PyType_FromMetaclass` (added in Python 3.12)
already disallows creating classes whose metaclass overrides ``tp_new``.

Deprecated
----------

Expand Down Expand Up @@ -1384,6 +1399,11 @@ Deprecated
* ``_PyErr_ChainExceptions`` is deprecated. Use ``_PyErr_ChainExceptions1``
instead. (Contributed by Irit Katriel in :gh:`102192`.)

* Using :c:func:`PyType_FromSpec`, :c:func:`PyType_FromSpecWithBases`
or :c:func:`PyType_FromModuleAndSpec` to create a class whose metaclass
overrides :c:member:`~PyTypeObject.tp_new` is deprecated.
Call the metaclass instead.

Removed
-------

Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,20 @@ def test_heaptype_with_custom_metaclass(self):
with self.assertRaisesRegex(TypeError, msg):
t = _testcapi.pytype_fromspec_meta(_testcapi.HeapCTypeMetaclassCustomNew)

def test_heaptype_with_custom_metaclass_deprecation(self):
# 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):
pass

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)

def test_multiple_inheritance_ctypes_with_weakref_or_dict(self):

with self.assertRaises(TypeError):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:c:func:`PyType_FromSpec` and its variants now allow creating classes whose
metaclass overrides :c:member:`~PyTypeObject.tp_new`. The ``tp_new`` is
ignored. This behavior is deprecated and will be disallowed in 3.14+. The
new :c:func:`PyType_FromMetaclass` already disallows it.
16 changes: 15 additions & 1 deletion Modules/_testcapi/heaptype.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ static PyObject *pytype_fromspec_meta(PyObject* self, PyObject *meta)
"_testcapi.HeapCTypeViaMetaclass",
sizeof(PyObject),
0,
Py_TPFLAGS_DEFAULT,
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
HeapCTypeViaMetaclass_slots
};

Expand Down Expand Up @@ -385,6 +385,19 @@ make_immutable_type_with_base(PyObject *self, PyObject *base)
return PyType_FromSpecWithBases(&ImmutableSubclass_spec, base);
}

static PyObject *
make_type_with_base(PyObject *self, PyObject *base)
{
assert(PyType_Check(base));
PyType_Spec ImmutableSubclass_spec = {
.name = "_testcapi.Subclass",
.basicsize = (int)((PyTypeObject*)base)->tp_basicsize,
.slots = empty_type_slots,
.flags = Py_TPFLAGS_DEFAULT,
};
return PyType_FromSpecWithBases(&ImmutableSubclass_spec, base);
}


static PyMethodDef TestMethods[] = {
{"pytype_fromspec_meta", pytype_fromspec_meta, METH_O},
Expand All @@ -397,6 +410,7 @@ static PyMethodDef TestMethods[] = {
test_from_spec_invalid_metatype_inheritance,
METH_NOARGS},
{"make_immutable_type_with_base", make_immutable_type_with_base, METH_O},
{"make_type_with_base", make_type_with_base, METH_O},
{NULL},
};

Expand Down
38 changes: 29 additions & 9 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -3830,9 +3830,10 @@ check_basicsize_includes_size_and_offsets(PyTypeObject* type)
return 1;
}

PyObject *
PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
PyType_Spec *spec, PyObject *bases_in)
static PyObject *
_PyType_FromMetaclass_impl(
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
PyTypeObject *metaclass, PyObject *module,
PyType_Spec *spec, PyObject *bases_in, int _allow_tp_new)
erlend-aasland marked this conversation as resolved.
Show resolved Hide resolved
{
/* Invariant: A non-NULL value in one of these means this function holds
* a strong reference or owns allocated memory.
Expand Down Expand Up @@ -4007,9 +4008,21 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
goto finally;
}
if (metaclass->tp_new != PyType_Type.tp_new) {
PyErr_SetString(PyExc_TypeError,
"Metaclasses with custom tp_new are not supported.");
goto finally;
if (_allow_tp_new) {
if (PyErr_WarnFormat(
PyExc_DeprecationWarning, 1,
"Using PyType_Spec with metaclasses that have custom "
"tp_new is deprecated and will no longer be allowed in "
"Python 3.14.") < 0) {
goto finally;
}
}
else {
PyErr_SetString(
PyExc_TypeError,
"Metaclasses with custom tp_new are not supported.");
goto finally;
}
}

/* Calculate best base, and check that all bases are type objects */
Expand Down Expand Up @@ -4196,22 +4209,29 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
return (PyObject*)res;
}

PyObject *
PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
PyType_Spec *spec, PyObject *bases_in)
{
return _PyType_FromMetaclass_impl(metaclass, module, spec, bases_in, 0);
}

PyObject *
PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
{
return PyType_FromMetaclass(NULL, module, spec, bases);
return _PyType_FromMetaclass_impl(NULL, module, spec, bases, 1);
}

PyObject *
PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
{
return PyType_FromMetaclass(NULL, NULL, spec, bases);
return _PyType_FromMetaclass_impl(NULL, NULL, spec, bases, 1);
}

PyObject *
PyType_FromSpec(PyType_Spec *spec)
{
return PyType_FromMetaclass(NULL, NULL, spec, NULL);
return _PyType_FromMetaclass_impl(NULL, NULL, spec, NULL, 1);
}

PyObject *
Expand Down