Skip to content

Commit dcaf33a

Browse files
encukouneoneneerlend-aasland
authored
gh-114314: ctypes: remove stgdict and switch to heap types (GH-116458)
Before this change, ctypes classes used a custom dict subclass, `StgDict`, as their `tp_dict`. This acts like a regular dict but also includes extra information about the type. This replaces stgdict by `StgInfo`, a C struct on the type, accessed by `PyObject_GetTypeData()` (PEP-697). All usage of `StgDict` (mainly variables named `stgdict`, `dict`, `edict` etc.) is converted to `StgInfo` (named `stginfo`, `info`, `einfo`, etc.). Where the dict is actually used for class attributes (as a regular PyDict), it's now called `attrdict`. This change -- not overriding `tp_dict` -- is made to make me comfortable with the next part of this PR: moving the initialization logic from `tp_new` to `tp_init`. The `StgInfo` is set up in `__init__` of each class, with a guard that prevents calling `__init__` more than once. Note that abstract classes (like `Array` or `Structure`) are created using `PyType_FromMetaclass` and do not have `__init__` called. Previously, this was done in `__new__`, which also wasn't called for abstract classes. Since `__init__` can be called from Python code or skipped, there is a tested guard to ensure `StgInfo` is initialized exactly once before it's used. Co-authored-by: neonene <[email protected]> Co-authored-by: Erlend E. Aasland <[email protected]>
1 parent 44fbab4 commit dcaf33a

15 files changed

+1496
-1411
lines changed

Lib/test/test_ctypes/test_arrays.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ def test_type_flags(self):
3737
self.assertTrue(cls.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
3838
self.assertFalse(cls.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
3939

40+
def test_metaclass_details(self):
41+
# Abstract classes (whose metaclass __init__ was not called) can't be
42+
# instantiated directly
43+
NewArray = PyCArrayType.__new__(PyCArrayType, 'NewArray', (Array,), {})
44+
for cls in Array, NewArray:
45+
with self.subTest(cls=cls):
46+
with self.assertRaisesRegex(TypeError, "abstract class"):
47+
obj = cls()
48+
49+
# Cannot call the metaclass __init__ more than once
50+
class T(Array):
51+
_type_ = c_int
52+
_length_ = 13
53+
with self.assertRaisesRegex(SystemError, "already initialized"):
54+
PyCArrayType.__init__(T, 'ptr', (), {})
55+
4056
def test_simple(self):
4157
# create classes holding simple numeric types, and check
4258
# various properties.

Lib/test/test_ctypes/test_callbacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_pyobject(self):
106106

107107
def test_unsupported_restype_1(self):
108108
# Only "fundamental" result types are supported for callback
109-
# functions, the type must have a non-NULL stgdict->setfunc.
109+
# functions, the type must have a non-NULL stginfo->setfunc.
110110
# POINTER(c_double), for example, is not supported.
111111

112112
prototype = self.functype.__func__(POINTER(c_double))

Lib/test/test_ctypes/test_funcptr.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def test_type_flags(self):
2929
self.assertTrue(_CFuncPtr.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
3030
self.assertFalse(_CFuncPtr.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
3131

32+
def test_metaclass_details(self):
33+
# Cannot call the metaclass __init__ more than once
34+
CdeclCallback = CFUNCTYPE(c_int, c_int, c_int)
35+
with self.assertRaisesRegex(SystemError, "already initialized"):
36+
PyCFuncPtrType.__init__(CdeclCallback, 'ptr', (), {})
37+
3238
def test_basic(self):
3339
X = WINFUNCTYPE(c_int, c_int, c_int)
3440

Lib/test/test_ctypes/test_pointers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ def test_type_flags(self):
3333
self.assertTrue(_Pointer.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
3434
self.assertFalse(_Pointer.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
3535

36+
def test_metaclass_details(self):
37+
# Cannot call the metaclass __init__ more than once
38+
with self.assertRaisesRegex(SystemError, "already initialized"):
39+
PyCPointerType.__init__(POINTER(c_byte), 'ptr', (), {})
40+
3641
def test_pointer_crash(self):
3742

3843
class A(POINTER(c_ulong)):

Lib/test/test_ctypes/test_simplesubclasses.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ def test_type_flags(self):
2626
self.assertTrue(_SimpleCData.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
2727
self.assertFalse(_SimpleCData.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
2828

29+
def test_metaclass_details(self):
30+
# Abstract classes (whose metaclass __init__ was not called) can't be
31+
# instantiated directly
32+
NewT = PyCSimpleType.__new__(PyCSimpleType, 'NewT', (_SimpleCData,), {})
33+
for cls in _SimpleCData, NewT:
34+
with self.subTest(cls=cls):
35+
with self.assertRaisesRegex(TypeError, "abstract class"):
36+
obj = cls()
37+
38+
# Cannot call the metaclass __init__ more than once
39+
class T(_SimpleCData):
40+
_type_ = "i"
41+
with self.assertRaisesRegex(SystemError, "already initialized"):
42+
PyCSimpleType.__init__(T, 'ptr', (), {})
43+
44+
def test_swapped_type_creation(self):
45+
cls = PyCSimpleType.__new__(PyCSimpleType, '', (), {'_type_': 'i'})
46+
with self.assertRaises(TypeError):
47+
PyCSimpleType.__init__(cls)
48+
PyCSimpleType.__init__(cls, '', (), {'_type_': 'i'})
49+
self.assertEqual(cls.__ctype_le__.__dict__.get('_type_'), 'i')
50+
self.assertEqual(cls.__ctype_be__.__dict__.get('_type_'), 'i')
51+
2952
def test_compare(self):
3053
self.assertEqual(MyInt(3), MyInt(3))
3154
self.assertNotEqual(MyInt(42), MyInt(43))

Lib/test/test_ctypes/test_struct_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def test_cfield_inheritance_hierarchy(self):
6969
def test_gh99275(self):
7070
class BrokenStructure(Structure):
7171
def __init_subclass__(cls, **kwargs):
72-
cls._fields_ = [] # This line will fail, `stgdict` is not ready
72+
cls._fields_ = [] # This line will fail, `stginfo` is not ready
7373

7474
with self.assertRaisesRegex(TypeError,
7575
'ctypes state is not initialized'):

Lib/test/test_ctypes/test_structures.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ def test_type_flags(self):
8585
self.assertTrue(Structure.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
8686
self.assertFalse(Structure.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
8787

88+
def test_metaclass_details(self):
89+
# Abstract classes (whose metaclass __init__ was not called) can't be
90+
# instantiated directly
91+
NewStructure = PyCStructType.__new__(PyCStructType, 'NewStructure',
92+
(Structure,), {})
93+
for cls in Structure, NewStructure:
94+
with self.subTest(cls=cls):
95+
with self.assertRaisesRegex(TypeError, "abstract class"):
96+
obj = cls()
97+
98+
# Cannot call the metaclass __init__ more than once
99+
class T(Structure):
100+
_fields_ = [("x", c_char),
101+
("y", c_char)]
102+
with self.assertRaisesRegex(SystemError, "already initialized"):
103+
PyCStructType.__init__(T, 'ptr', (), {})
104+
88105
def test_simple_structs(self):
89106
for code, tp in self.formats.items():
90107
class X(Structure):
@@ -507,8 +524,8 @@ def _test_issue18060(self, Vector):
507524
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
508525
def test_issue18060_a(self):
509526
# This test case calls
510-
# PyCStructUnionType_update_stgdict() for each
511-
# _fields_ assignment, and PyCStgDict_clone()
527+
# PyCStructUnionType_update_stginfo() for each
528+
# _fields_ assignment, and PyCStgInfo_clone()
512529
# for the Mid and Vector class definitions.
513530
class Base(Structure):
514531
_fields_ = [('y', c_double),
@@ -523,7 +540,7 @@ class Vector(Mid): pass
523540
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
524541
def test_issue18060_b(self):
525542
# This test case calls
526-
# PyCStructUnionType_update_stgdict() for each
543+
# PyCStructUnionType_update_stginfo() for each
527544
# _fields_ assignment.
528545
class Base(Structure):
529546
_fields_ = [('y', c_double),
@@ -538,7 +555,7 @@ class Vector(Mid):
538555
@unittest.skipUnless(sys.byteorder == 'little', "can't test on this platform")
539556
def test_issue18060_c(self):
540557
# This test case calls
541-
# PyCStructUnionType_update_stgdict() for each
558+
# PyCStructUnionType_update_stginfo() for each
542559
# _fields_ assignment.
543560
class Base(Structure):
544561
_fields_ = [('y', c_double)]

Lib/test/test_ctypes/test_unions.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from ctypes import Union
2+
from ctypes import Union, c_char
33
from ._support import (_CData, UnionType, Py_TPFLAGS_DISALLOW_INSTANTIATION,
44
Py_TPFLAGS_IMMUTABLETYPE)
55

@@ -16,3 +16,20 @@ def test_type_flags(self):
1616
with self.subTest(cls=Union):
1717
self.assertTrue(Union.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
1818
self.assertFalse(Union.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)
19+
20+
def test_metaclass_details(self):
21+
# Abstract classes (whose metaclass __init__ was not called) can't be
22+
# instantiated directly
23+
NewUnion = UnionType.__new__(UnionType, 'NewUnion',
24+
(Union,), {})
25+
for cls in Union, NewUnion:
26+
with self.subTest(cls=cls):
27+
with self.assertRaisesRegex(TypeError, "abstract class"):
28+
obj = cls()
29+
30+
# Cannot call the metaclass __init__ more than once
31+
class T(Union):
32+
_fields_ = [("x", c_char),
33+
("y", c_char)]
34+
with self.assertRaisesRegex(SystemError, "already initialized"):
35+
UnionType.__init__(T, 'ptr', (), {})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In :mod:`ctypes`, ctype data is now stored in type objects directly rather
2+
than in a dict subclass. This is an internal change that should not affect
3+
usage.

0 commit comments

Comments
 (0)