Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Implement the feature
  • Loading branch information
neonene committed Sep 17, 2024
commit 997e602b659db0aee2f649aed62bac8b4aab16f6
68 changes: 67 additions & 1 deletion Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,24 @@ Type Objects

.. versionadded:: 3.11

.. c:function:: int PyType_GetBaseByToken(PyTypeObject *type, void *token, PyTypeObject **result)

Find the first superclass in *type*'s :term:`method resolution order` whose
:c:macro:`Py_tp_token` token is equal to the given one.

* If found, set *\*result* to a new :term:`strong reference`
to it and return ``1``.
* If not found, set *\*result* to ``NULL`` and return ``0``.
* On error, set *\*result* to ``NULL`` and return ``-1`` with an
exception set.

The *result* argument may be ``NULL``, in which case *\*result* is not set.
Use this if you need only the return value.

The *token* argument may not be ``NULL``.

.. versionadded:: 3.14

.. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type)

Attempt to assign a version tag to the given type.
Expand Down Expand Up @@ -488,6 +506,11 @@ The following functions and structs are used to create
* ``Py_nb_add`` to set :c:member:`PyNumberMethods.nb_add`
* ``Py_sq_length`` to set :c:member:`PySequenceMethods.sq_length`

An additional slot is supported that does not correspond to a
:c:type:`!PyTypeObject` struct field:

* :c:data:`Py_tp_token`

The following “offset” fields cannot be set using :c:type:`PyType_Slot`:

* :c:member:`~PyTypeObject.tp_weaklistoffset`
Expand Down Expand Up @@ -538,4 +561,47 @@ The following functions and structs are used to create
The desired value of the slot. In most cases, this is a pointer
to a function.

Slots other than ``Py_tp_doc`` may not be ``NULL``.
*pfunc* values may not be ``NULL``, except for the following slots:

* ``Py_tp_doc``
* :c:data:`Py_tp_token` (for clarity, prefer :c:data:`Py_TP_USE_SPEC`
rather than ``NULL``)

.. c:macro:: Py_tp_token

A :c:member:`~PyType_Slot.slot` that records a static memory layout ID
for a class.

If the :c:type:`PyType_Spec` of the class is statically
allocated, the token can be set to the spec using the special value
:c:data:`Py_TP_USE_SPEC`:

.. code-block:: c

static PyType_Slot foo_slots[] = {
{Py_tp_token, Py_TP_USE_SPEC},

It can also be set to an arbitrary pointer, but you must ensure that:

* The pointer outlives the class, so it's not reused for something else
while the class exists.
* It "belongs" to the extension module where the class lives, so it will not
clash with other extensions.

Use :c:func:`PyType_GetBaseByToken` to check if a class's superclass has
a given token -- that is, check whether the memory layout is compatible.

To get the token for a given class (without considering superclasses),
use :c:func:`PyType_GetSlot` with ``Py_tp_token``.

.. versionadded:: 3.14

.. c:namespace:: NULL

.. c:macro:: Py_TP_USE_SPEC

Used as a value with :c:data:`Py_tp_token` to set the token to the
class's :c:type:`PyType_Spec`.
Expands to ``NULL``.

.. versionadded:: 3.14
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,11 @@ New Features

(Contributed by Victor Stinner in :gh:`107954`.)

* Add :c:func:`PyType_GetBaseByToken` and :c:data:`Py_tp_token` slot for easier
superclass identification, which attempts to resolve the `type checking issue
<https://peps.python.org/pep-0630/#type-checking>`__ mentioned in :pep:`630`
(:gh:`121079`).


Porting to Python 3.14
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ typedef struct _heaptypeobject {
struct _dictkeysobject *ht_cached_keys;
PyObject *ht_module;
char *_ht_tpname; // Storage for "tp_name"; see PyType_FromModuleAndSpec
void *ht_token; // Storage for the "Py_tp_token" slot
struct _specialization_cache _spec_cache; // For use by the specializer.
#ifdef Py_GIL_DISABLED
Py_ssize_t unique_id; // ID used for thread-local refcounting
Expand Down
4 changes: 4 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ PyAPI_FUNC(PyObject *) PyType_FromMetaclass(PyTypeObject*, PyObject*, PyType_Spe
PyAPI_FUNC(void *) PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls);
PyAPI_FUNC(Py_ssize_t) PyType_GetTypeDataSize(PyTypeObject *cls);
#endif
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000
PyAPI_FUNC(int) PyType_GetBaseByToken(PyTypeObject *, void *, PyTypeObject **);
#define Py_TP_USE_SPEC NULL
#endif

/* Generic type check */
PyAPI_FUNC(int) PyType_IsSubtype(PyTypeObject *, PyTypeObject *);
Expand Down
4 changes: 4 additions & 0 deletions Include/typeslots.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@
/* New in 3.14 */
#define Py_tp_vectorcall 82
#endif
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030E0000
/* New in 3.14 */
#define Py_tp_token 83
#endif
71 changes: 71 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,77 @@ class MyType:
MyType.__module__ = 123
self.assertEqual(get_type_fullyqualname(MyType), 'my_qualname')

def test_get_base_by_token(self):
def get_base_by_token(src, key, comparable=True):
def run(use_mro):
find_first = _testcapi.pytype_getbasebytoken
ret1, result = find_first(src, key, use_mro, True)
ret2, no_result = find_first(src, key, use_mro, False)
self.assertIn(ret1, (0, 1))
self.assertEqual(ret1, result is not None)
self.assertEqual(ret1, ret2)
self.assertIsNone(no_result)
return result

found_in_mro = run(True)
found_in_bases = run(False)
if comparable:
self.assertIs(found_in_mro, found_in_bases)
return found_in_mro
return found_in_mro, found_in_bases

create_type = _testcapi.create_type_with_token
get_token = _testcapi.get_tp_token

Py_TP_USE_SPEC = _testcapi.Py_TP_USE_SPEC
self.assertEqual(Py_TP_USE_SPEC, 0)

A1 = create_type('_testcapi.A1', Py_TP_USE_SPEC)
self.assertTrue(get_token(A1) != Py_TP_USE_SPEC)

B1 = create_type('_testcapi.B1', id(self))
self.assertTrue(get_token(B1) == id(self))

tokenA1 = get_token(A1)
# find A1 from A1
found = get_base_by_token(A1, tokenA1)
self.assertIs(found, A1)

# no token in static types
STATIC = type(1)
self.assertEqual(get_token(STATIC), 0)
found = get_base_by_token(STATIC, tokenA1)
self.assertIs(found, None)

# no token in pure subtypes
class A2(A1): pass
self.assertEqual(get_token(A2), 0)
# find A1
class Z(STATIC, B1, A2): pass
found = get_base_by_token(Z, tokenA1)
self.assertIs(found, A1)

# searching for NULL token is an error
with self.assertRaises(SystemError):
get_base_by_token(Z, 0)
with self.assertRaises(SystemError):
get_base_by_token(STATIC, 0)

# share the token with A1
C1 = create_type('_testcapi.C1', tokenA1)
self.assertTrue(get_token(C1) == tokenA1)

# find C1 first by shared token
class Z(C1, A2): pass
found = get_base_by_token(Z, tokenA1)
self.assertIs(found, C1)
# B1 not found
found = get_base_by_token(Z, get_token(B1))
self.assertIs(found, None)

with self.assertRaises(TypeError):
_testcapi.pytype_getbasebytoken(
'not a type', id(self), True, False)

def test_gen_get_code(self):
def genf(): yield
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,7 +1718,7 @@ def delx(self): del self.__x
'3P' # PyMappingMethods
'10P' # PySequenceMethods
'2P' # PyBufferProcs
'6P'
'7P'
'1PIP' # Specializer cache
+ typeid # heap type id (free-threaded only)
)
Expand Down
8 changes: 7 additions & 1 deletion Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2527,4 +2527,10 @@
[function.PyLong_AsUInt64]
added = '3.14'
[const.Py_tp_vectorcall]
added = '3.14'
added = '3.14'
[function.PyType_GetBaseByToken]
added = '3.14'
[const.Py_tp_token]
added = '3.14'
[const.Py_TP_USE_SPEC]
added = '3.14'
117 changes: 117 additions & 0 deletions Modules/_testcapi/heaptype.c
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,118 @@ pyobject_getitemdata(PyObject *self, PyObject *o)
}


static PyObject *
create_type_with_token(PyObject *module, PyObject *args)
{
const char *name;
PyObject *py_token;
if (!PyArg_ParseTuple(args, "sO", &name, &py_token)) {
return NULL;
}
void *token = PyLong_AsVoidPtr(py_token);
if (token == Py_TP_USE_SPEC) {
// Py_TP_USE_SPEC requires the spec that at least outlives the class
static PyType_Slot slots[] = {
{Py_tp_token, Py_TP_USE_SPEC},
{0},
};
static PyType_Spec spec = {
.name = "_testcapi.DefaultTokenTest",
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = slots,
};
PyObject *type = PyType_FromMetaclass(NULL, NULL, &spec, NULL);
if (!type) {
return NULL;
}
token = PyType_GetSlot((PyTypeObject *)type, Py_tp_token);
assert(!PyErr_Occurred());
Py_DECREF(type);
if (token != &spec) {
PyErr_SetString(PyExc_AssertionError,
"failed to convert token from Py_TP_USE_SPEC");
return NULL;
}
}
// Test non-NULL token that must also outlive the class
PyType_Slot slots[] = {
{Py_tp_token, token},
{0},
};
PyType_Spec spec = {
.name = name,
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = slots,
};
return PyType_FromMetaclass(NULL, module, &spec, NULL);
}

static PyObject *
get_tp_token(PyObject *self, PyObject *type)
{
void *token = PyType_GetSlot((PyTypeObject *)type, Py_tp_token);
if (PyErr_Occurred()) {
return NULL;
}
return PyLong_FromVoidPtr(token);
}

static PyObject *
pytype_getbasebytoken(PyObject *self, PyObject *args)
{
PyTypeObject *type;
PyObject *py_token, *use_mro, *need_result;
if (!PyArg_ParseTuple(args, "OOOO",
&type, &py_token, &use_mro, &need_result)) {
return NULL;
}

PyObject *mro_save = NULL;
if (use_mro != Py_True) {
// Test internal detail: PyType_GetBaseByToken works even with
// types that are only partially initialized (or torn down):
// if tp_mro=NULL we fall back to tp_bases.
assert(PyType_Check(type));
mro_save = type->tp_mro;
type->tp_mro = NULL;
}

void *token = PyLong_AsVoidPtr(py_token);
PyObject *result;
int ret;
if (need_result == Py_True) {
ret = PyType_GetBaseByToken(type, token, (PyTypeObject **)&result);
}
else {
result = NULL;
ret = PyType_GetBaseByToken(type, token, NULL);
}

if (use_mro != Py_True) {
type->tp_mro = mro_save;
}
if (ret < 0) {
assert(result == NULL);
return NULL;
}
PyObject *py_ret = PyLong_FromLong(ret);
if (py_ret == NULL) {
goto error;
}
PyObject *tuple = PyTuple_New(2);
if (tuple == NULL) {
goto error;
}
PyTuple_SET_ITEM(tuple, 0, py_ret);
PyTuple_SET_ITEM(tuple, 1, result ? result : Py_None);
return tuple;
error:
Py_XDECREF(py_ret);
Py_XDECREF(result);
return NULL;
}


static PyMethodDef TestMethods[] = {
{"pytype_fromspec_meta", pytype_fromspec_meta, METH_O},
{"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS},
Expand All @@ -423,6 +535,9 @@ static PyMethodDef TestMethods[] = {
{"make_immutable_type_with_base", make_immutable_type_with_base, METH_O},
{"make_type_with_base", make_type_with_base, METH_O},
{"pyobject_getitemdata", pyobject_getitemdata, METH_O},
{"create_type_with_token", create_type_with_token, METH_VARARGS},
{"get_tp_token", get_tp_token, METH_O},
{"pytype_getbasebytoken", pytype_getbasebytoken, METH_VARARGS},
{NULL},
};

Expand Down Expand Up @@ -1287,6 +1402,8 @@ _PyTestCapi_Init_Heaptype(PyObject *m) {
&PyType_Type, m, &HeapCTypeMetaclassNullNew_spec, (PyObject *) &PyType_Type);
ADD("HeapCTypeMetaclassNullNew", HeapCTypeMetaclassNullNew);

ADD("Py_TP_USE_SPEC", PyLong_FromVoidPtr(Py_TP_USE_SPEC));

PyObject *HeapCCollection = PyType_FromMetaclass(
NULL, m, &HeapCCollection_spec, NULL);
if (HeapCCollection == NULL) {
Expand Down
Loading