gh-100980: ctypes: Test, document, and fix finalizing _fields_ (GH-124292)

- If setting `_fields_` fails, e.g. with AttributeError, don't set the attribute in `__dict__`
- Document the “finalization” behaviour
- Beef up tests: add `getattr`, test Union as well as Structure
- Put common functionality in a common function

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
This commit is contained in:
Petr Viktorin 2024-09-24 02:40:53 +02:00 committed by GitHub
parent e256a7590a
commit be76e3f26e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 68 additions and 56 deletions

View File

@ -2499,6 +2499,8 @@ Structured data types
Abstract base class for unions in native byte order.
Unions share common attributes and behavior with structures;
see :class:`Structure` documentation for details.
.. class:: BigEndianUnion(*args, **kw)
@ -2558,14 +2560,19 @@ fields, or any other data types containing pointer type fields.
...
]
The :attr:`_fields_` class variable must, however, be defined before the
type is first used (an instance is created, :func:`sizeof` is called on it,
and so on). Later assignments to the :attr:`_fields_` class variable will
raise an AttributeError.
The :attr:`!_fields_` class variable can only be set once.
Later assignments will raise an :exc:`AttributeError`.
It is possible to define sub-subclasses of structure types, they inherit
the fields of the base class plus the :attr:`_fields_` defined in the
sub-subclass, if any.
Additionally, the :attr:`!_fields_` class variable must be defined before
the structure or union type is first used: an instance or subclass is
created, :func:`sizeof` is called on it, and so on.
Later assignments to :attr:`!_fields_` will raise an :exc:`AttributeError`.
If :attr:`!_fields_` has not been set before such use,
the structure or union will have no own fields, as if :attr:`!_fields_`
was empty.
Sub-subclasses of structure types inherit the fields of the base class
plus the :attr:`_fields_` defined in the sub-subclass, if any.
.. attribute:: _pack_

View File

@ -4,7 +4,9 @@ from ._support import (CField, Py_TPFLAGS_DISALLOW_INSTANTIATION,
Py_TPFLAGS_IMMUTABLETYPE)
class StructFieldsTestCase(unittest.TestCase):
NOTHING = object()
class FieldsTestBase:
# Structure/Union classes must get 'finalized' sooner or
# later, when one of these things happen:
#
@ -14,42 +16,47 @@ class StructFieldsTestCase(unittest.TestCase):
# 4. The type is subclassed
#
# When they are finalized, assigning _fields_ is no longer allowed.
def assert_final_fields(self, cls, expected=NOTHING):
self.assertRaises(AttributeError, setattr, cls, "_fields_", [])
self.assertEqual(getattr(cls, "_fields_", NOTHING), expected)
def test_1_A(self):
class X(Structure):
class X(self.cls):
pass
self.assertEqual(sizeof(X), 0) # not finalized
X._fields_ = [] # finalized
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X, expected=[])
def test_1_B(self):
class X(Structure):
class X(self.cls):
_fields_ = [] # finalized
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X, expected=[])
def test_2(self):
class X(Structure):
class X(self.cls):
pass
X()
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X)
def test_3(self):
class X(Structure):
class X(self.cls):
pass
class Y(Structure):
class Y(self.cls):
_fields_ = [("x", X)] # finalizes X
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X)
def test_4(self):
class X(Structure):
class X(self.cls):
pass
class Y(X):
pass
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X)
Y._fields_ = []
self.assertRaises(AttributeError, setattr, X, "_fields_", [])
self.assert_final_fields(X)
def test_5(self):
class X(Structure):
class X(self.cls):
_fields_ = (("char", c_char * 5),)
x = X(b'#' * 5)
@ -59,14 +66,8 @@ class StructFieldsTestCase(unittest.TestCase):
def test_6(self):
self.assertRaises(TypeError, CField)
def test_cfield_type_flags(self):
self.assertTrue(CField.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
def test_cfield_inheritance_hierarchy(self):
self.assertEqual(CField.mro(), [CField, object])
def test_gh99275(self):
class BrokenStructure(Structure):
class BrokenStructure(self.cls):
def __init_subclass__(cls, **kwargs):
cls._fields_ = [] # This line will fail, `stginfo` is not ready
@ -77,26 +78,28 @@ class StructFieldsTestCase(unittest.TestCase):
# __set__ and __get__ should raise a TypeError in case their self
# argument is not a ctype instance.
def test___set__(self):
class MyCStruct(Structure):
class MyCStruct(self.cls):
_fields_ = (("field", c_int),)
self.assertRaises(TypeError,
MyCStruct.field.__set__, 'wrong type self', 42)
class MyCUnion(Union):
_fields_ = (("field", c_int),)
self.assertRaises(TypeError,
MyCUnion.field.__set__, 'wrong type self', 42)
def test___get__(self):
class MyCStruct(Structure):
class MyCStruct(self.cls):
_fields_ = (("field", c_int),)
self.assertRaises(TypeError,
MyCStruct.field.__get__, 'wrong type self', 42)
class MyCUnion(Union):
_fields_ = (("field", c_int),)
self.assertRaises(TypeError,
MyCUnion.field.__get__, 'wrong type self', 42)
class StructFieldsTestCase(unittest.TestCase, FieldsTestBase):
cls = Structure
def test_cfield_type_flags(self):
self.assertTrue(CField.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
def test_cfield_inheritance_hierarchy(self):
self.assertEqual(CField.mro(), [CField, object])
class UnionFieldsTestCase(unittest.TestCase, FieldsTestBase):
cls = Union
if __name__ == "__main__":

View File

@ -0,0 +1,3 @@
The :attr:`~ctypes.Structure._fields_` attribute of
:class:`ctypes.Structure` and :class:`~ctypes.Union` is no longer set if
the setattr operation raises an error.

View File

@ -1067,32 +1067,31 @@ CType_Type_repeat(PyObject *self, Py_ssize_t length)
return PyCArrayType_from_ctype(st, self, length);
}
static int
_structunion_setattro(PyObject *self, PyObject *key, PyObject *value, int is_struct)
{
/* XXX Should we disallow deleting _fields_? */
if (PyUnicode_Check(key)
&& _PyUnicode_EqualToASCIIString(key, "_fields_"))
{
if (PyCStructUnionType_update_stginfo(self, value, is_struct) < 0) {
return -1;
}
}
return PyType_Type.tp_setattro(self, key, value);
}
static int
PyCStructType_setattro(PyObject *self, PyObject *key, PyObject *value)
{
/* XXX Should we disallow deleting _fields_? */
if (-1 == PyType_Type.tp_setattro(self, key, value))
return -1;
if (value && PyUnicode_Check(key) &&
_PyUnicode_EqualToASCIIString(key, "_fields_"))
return PyCStructUnionType_update_stginfo(self, value, 1);
return 0;
return _structunion_setattro(self, key, value, 1);
}
static int
UnionType_setattro(PyObject *self, PyObject *key, PyObject *value)
{
/* XXX Should we disallow deleting _fields_? */
if (-1 == PyType_Type.tp_setattro(self, key, value))
return -1;
if (PyUnicode_Check(key) &&
_PyUnicode_EqualToASCIIString(key, "_fields_"))
return PyCStructUnionType_update_stginfo(self, value, 0);
return 0;
return _structunion_setattro(self, key, value, 0);
}
static PyType_Slot pycstruct_type_slots[] = {