Skip to content
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
25 changes: 24 additions & 1 deletion Lib/test/test_ctypes/test_simplesubclasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData
from collections.abc import Hashable
from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData, POINTER
from ._support import (_CData, PyCSimpleType, Py_TPFLAGS_DISALLOW_INSTANTIATION,
Py_TPFLAGS_IMMUTABLETYPE)

Expand All @@ -26,6 +27,28 @@ def test_type_flags(self):
self.assertTrue(_SimpleCData.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
self.assertFalse(_SimpleCData.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)

def test_unhashable(self):
# gh-78327: CData objects are mutable and must not be hashable.
class S(Structure):
_fields_ = [("a", c_int)]

for cls in c_int, S, c_int * 2, POINTER(c_int), CFUNCTYPE(None):
with self.subTest(cls=cls):
self.assertIsNone(cls.__hash__)
obj = cls()
self.assertNotIsInstance(obj, Hashable)
with self.assertRaisesRegex(TypeError,
f"unhashable type: '{cls.__name__}'"):
hash(obj)

# Subclasses can still opt in to hashing.
class H(S):
def __hash__(self):
return 42

self.assertEqual(hash(H()), 42)
self.assertIsInstance(H(), Hashable)

def test_metaclass_details(self):
# Abstract classes (whose metaclass __init__ was not called) can't be
# instantiated directly
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`ctypes` data types now set :meth:`~object.__hash__` to ``None``, so
:class:`collections.abc.Hashable` checks report them correctly.
68 changes: 38 additions & 30 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -3105,16 +3105,6 @@ PyCData_NewGetBuffer(PyObject *myself, Py_buffer *view, int flags)
return 0;
}

/*
* CData objects are mutable, so they cannot be hashable!
*/
static Py_hash_t
PyCData_nohash(PyObject *self)
{
PyErr_SetString(PyExc_TypeError, "unhashable type");
return -1;
}

/*[clinic input]
@critical_section
_ctypes.PyCData.__reduce__
Expand Down Expand Up @@ -3211,25 +3201,40 @@ static PyMethodDef PyCData_methods[] = {
{ NULL, NULL },
};

static PyType_Slot pycdata_slots[] = {
{Py_tp_dealloc, PyCData_dealloc},
{Py_tp_hash, PyCData_nohash},
{Py_tp_doc, PyDoc_STR("XXX to be provided")},
{Py_tp_traverse, PyCData_traverse},
{Py_tp_clear, PyCData_clear},
{Py_tp_methods, PyCData_methods},
{Py_tp_members, PyCData_members},
{Py_bf_getbuffer, PyCData_NewGetBuffer},
{0, NULL},
};

static PyType_Spec pycdata_spec = {
.name = "_ctypes._CData",
.basicsize = sizeof(CDataObject),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION),
.slots = pycdata_slots,
};
/*
* CData objects are mutable, so they cannot be hashable. Setting tp_hash to
* PyObject_HashNotImplemented() makes type_ready() set __hash__ to None.
*
* Unfortunately, on Windows, PyObject_HashNotImplemented is imported from
* the Python DLL, and MSVC resolves the address of an imported function in
* a static initializer to a local thunk that can't be recognized. So, we
* initialize the slots at runtime to load the real address from the import table.
*/
static PyObject *
PyCData_create_type(PyObject *mod)
{
PyType_Slot slots[] = {
{Py_tp_dealloc, PyCData_dealloc},
{Py_tp_hash, PyObject_HashNotImplemented},
{Py_tp_doc, PyDoc_STR("The common base class of all ctypes "
"data types.")},
{Py_tp_traverse, PyCData_traverse},
{Py_tp_clear, PyCData_clear},
{Py_tp_methods, PyCData_methods},
{Py_tp_members, PyCData_members},
{Py_bf_getbuffer, PyCData_NewGetBuffer},
{0, NULL},
};
PyType_Spec spec = {
.name = "_ctypes._CData",
.basicsize = sizeof(CDataObject),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE |
Py_TPFLAGS_DISALLOW_INSTANTIATION),
.slots = slots,
};
return PyType_FromMetaclass(NULL, mod, &spec, NULL);
}

static int
PyCData_MallocBuffer(CDataObject *obj, StgInfo *info)
Expand Down Expand Up @@ -6249,7 +6254,10 @@ _ctypes_add_types(PyObject *mod)
*/
CREATE_TYPE(st->PyCArg_Type, &carg_spec, NULL, NULL);
CREATE_TYPE(st->PyCThunk_Type, &cthunk_spec, NULL, NULL);
CREATE_TYPE(st->PyCData_Type, &pycdata_spec, NULL, NULL);
st->PyCData_Type = (PyTypeObject *)PyCData_create_type(mod);
if (st->PyCData_Type == NULL) {
return -1;
}

// Common Metaclass
CREATE_TYPE(st->PyCType_Type, &pyctype_type_spec,
Expand Down
Loading