Skip to content

Commit

Permalink
pythongh-124153: Implement PyType_GetBaseByToken() and `Py_tp_token…
Browse files Browse the repository at this point in the history
…` slot (pythonGH-124163)
  • Loading branch information
neonene authored and savannahostrowski committed Sep 22, 2024
1 parent 4e65bba commit 6da8650
Show file tree
Hide file tree
Showing 18 changed files with 443 additions and 13 deletions.
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:`124153`).


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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyType_GetBaseByToken` and :c:data:`Py_tp_token` slot for easier
type checking, related to :pep:`489` and :pep:`630`.
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'
5 changes: 3 additions & 2 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ CType_Type_dealloc(PyObject *self)
{
StgInfo *info = _PyStgInfo_FromType_NoState(self);
if (!info) {
PyErr_WriteUnraisable(self);
PyErr_WriteUnraisable(NULL); // NULL avoids segfault here
}
if (info) {
PyMem_Free(info->ffi_type_pointer.elements);
Expand Down Expand Up @@ -560,6 +560,7 @@ static PyMethodDef ctype_methods[] = {
};

static PyType_Slot ctype_type_slots[] = {
{Py_tp_token, Py_TP_USE_SPEC},
{Py_tp_traverse, CType_Type_traverse},
{Py_tp_clear, CType_Type_clear},
{Py_tp_dealloc, CType_Type_dealloc},
Expand All @@ -569,7 +570,7 @@ static PyType_Slot ctype_type_slots[] = {
{0, NULL},
};

static PyType_Spec pyctype_type_spec = {
PyType_Spec pyctype_type_spec = {
.name = "_ctypes.CType_Type",
.basicsize = -(Py_ssize_t)sizeof(StgInfo),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE |
Expand Down
20 changes: 14 additions & 6 deletions Modules/_ctypes/ctypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ get_module_state_by_def(PyTypeObject *cls)
}


extern PyType_Spec pyctype_type_spec;
extern PyType_Spec carg_spec;
extern PyType_Spec cfield_spec;
extern PyType_Spec cthunk_spec;
Expand Down Expand Up @@ -490,16 +491,23 @@ PyStgInfo_FromAny(ctypes_state *state, PyObject *obj, StgInfo **result)

/* A variant of PyStgInfo_FromType that doesn't need the state,
* so it can be called from finalization functions when the module
* state is torn down. Does no checks; cannot fail.
* This inlines the current implementation PyObject_GetTypeData,
* so it might break in the future.
* state is torn down.
*/
static inline StgInfo *
_PyStgInfo_FromType_NoState(PyObject *type)
{
size_t type_basicsize =_Py_SIZE_ROUND_UP(PyType_Type.tp_basicsize,
ALIGNOF_MAX_ALIGN_T);
return (StgInfo *)((char *)type + type_basicsize);
PyTypeObject *PyCType_Type;
if (PyType_GetBaseByToken(Py_TYPE(type), &pyctype_type_spec, &PyCType_Type) < 0) {
return NULL;
}
if (PyCType_Type == NULL) {
PyErr_Format(PyExc_TypeError, "expected a ctypes type, got '%N'", type);
return NULL;
}

StgInfo *info = PyObject_GetTypeData(type, PyCType_Type);
Py_DECREF(PyCType_Type);
return info;
}

// Initialize StgInfo on a newly created type
Expand Down
Loading

0 comments on commit 6da8650

Please sign in to comment.