]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-104533: Fix `@ctypes.util.struct` with endian (#154038)
authorVictor Stinner <vstinner@python.org>
Sun, 19 Jul 2026 10:07:43 +0000 (12:07 +0200)
committerGitHub <noreply@github.com>
Sun, 19 Jul 2026 10:07:43 +0000 (12:07 +0200)
Change `_fields_` type to a list of tuples, instead of a list of lists.

Modify test.test_ctypes.test_aligned_structures to test also
`@ctypes.util.struct` decorator.

Lib/ctypes/util.py
Lib/test/test_ctypes/test_aligned_structures.py

index e8db8c578bb36014235352d2c047128ed6b899a6..a73422598b2cd96160282b49c5b12a6fcf435ffa 100644 (file)
@@ -532,7 +532,8 @@ def _process_struct(decorated_class, /, *, align, layout, endian, pack):
         else:
             field.append(hint)
 
-        fields.append(field)
+        # _fields_ is a list of tuples
+        fields.append(tuple(field))
 
     if endian == 'big':
         endian_class = BigEndianStructure
index 50b4d729b9db8adad97413d8d018d34110b5857e..d58d92a19eb33c999568dc9b22e672c08773a5fd 100644 (file)
@@ -3,29 +3,64 @@ from ctypes import (
     BigEndianStructure, LittleEndianStructure,
     BigEndianUnion, LittleEndianUnion, Structure,
 )
+from ctypes.util import struct as struct_util
 import struct
 import unittest
 from ._support import StructCheckMixin
+from test.support import subTests
+
+
+def get_struct_base(endian):
+    if endian == 'big':
+        return BigEndianStructure
+    elif endian == 'little':
+        return LittleEndianStructure
+    elif endian == 'native':
+        return Structure
+    else:
+        raise ValueError('invalid endian')
+
+def get_union_base(endian):
+    if endian == 'big':
+        return BigEndianUnion
+    elif endian == 'little':
+        return LittleEndianUnion
+    else:
+        raise ValueError('invalid endian')
+
 
 class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
-    def test_aligned_string(self):
-        for base, e in (
-            (LittleEndianStructure, "<"),
-            (BigEndianStructure, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_string(self, use_struct_util):
+        for endian, e in (
+            ('little', "<"),
+            ('big', ">"),
         ):
             data =  bytearray(struct.pack(f"{e}i12x16s", 7, b"hello world!"))
-            class Aligned(base):
-                _align_ = 16
-                _fields_ = [
-                    ('value', c_char * 12)
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian, align=16)
+                class Aligned:
+                    value: c_char * 12
+            else:
+                base = get_struct_base(endian)
+                class Aligned(base):
+                    _align_ = 16
+                    _fields_ = [
+                        ('value', c_char * 12)
+                    ]
             self.check_struct(Aligned)
 
-            class Main(base):
-                _fields_ = [
-                    ('first', c_uint32),
-                    ('string', Aligned),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian)
+                class Main:
+                    first: c_uint32
+                    string: Aligned
+            else:
+                class Main(base):
+                    _fields_ = [
+                        ('first', c_uint32),
+                        ('string', Aligned),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -37,24 +72,39 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(alignment(main.string), 16)
             self.assertEqual(alignment(main), 16)
 
-    def test_aligned_structures(self):
-        for base, data in (
-            (LittleEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
-            (BigEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_structures(self, use_struct_util):
+        for endian, data in (
+            ('little', bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
+            ('big', bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
         ):
-            class SomeBools(base):
-                _align_ = 4
-                _fields_ = [
-                    ("bool1", c_ubyte),
-                    ("bool2", c_ubyte),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian, align=4)
+                class SomeBools:
+                    bool1: c_ubyte
+                    bool2: c_ubyte
+            else:
+                base = get_struct_base(endian)
+                class SomeBools(base):
+                    _align_ = 4
+                    _fields_ = [
+                        ("bool1", c_ubyte),
+                        ("bool2", c_ubyte),
+                    ]
             self.check_struct(SomeBools)
-            class Main(base):
-                _fields_ = [
-                    ("x", c_ubyte),
-                    ("y", SomeBools),
-                    ("z", c_ubyte),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian)
+                class Main:
+                    x: c_ubyte
+                    y: SomeBools
+                    z: c_ubyte
+            else:
+                class Main(base):
+                    _fields_ = [
+                        ("x", c_ubyte),
+                        ("y", SomeBools),
+                        ("z", c_ubyte),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -69,57 +119,93 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(Main.z.offset, 8)
             self.assertEqual(main.z, 7)
 
-    def test_negative_align(self):
-        for base in (Structure, LittleEndianStructure, BigEndianStructure):
+    @subTests("use_struct_util", [False, True])
+    def test_negative_align(self, use_struct_util):
+        for endian in ('native', 'little', 'big'):
             with (
-                self.subTest(base=base),
+                self.subTest(endian=endian),
                 self.assertRaisesRegex(
                     ValueError,
                     '_align_ must be a non-negative integer',
                 )
             ):
-                class MyStructure(base):
-                    _align_ = -1
-                    _fields_ = []
-
-    def test_zero_align_no_fields(self):
-        for base in (Structure, LittleEndianStructure, BigEndianStructure):
-            with self.subTest(base=base):
-                class MyStructure(base):
-                    _align_ = 0
-                    _fields_ = []
+                if use_struct_util:
+                    @struct_util(endian=endian, align=-1)
+                    class MyStructure:
+                        pass
+                else:
+                    base = get_struct_base(endian)
+                    class MyStructure(base):
+                        _align_ = -1
+                        _fields_ = []
+
+    @subTests("use_struct_util", [False, True])
+    def test_zero_align_no_fields(self, use_struct_util):
+        for endian in ('native', 'little', 'big'):
+            with self.subTest(endian=endian):
+                if use_struct_util:
+                    @struct_util(endian=endian, align=0)
+                    class MyStructure:
+                        pass
+                else:
+                    base = get_struct_base(endian)
+                    class MyStructure(base):
+                        _align_ = 0
+                        _fields_ = []
 
                 self.assertEqual(alignment(MyStructure), 1)
                 self.assertEqual(alignment(MyStructure()), 1)
 
-    def test_zero_align_with_fields(self):
-        for base in (Structure, LittleEndianStructure, BigEndianStructure):
-            with self.subTest(base=base):
-                class MyStructure(base):
-                    _align_ = 0
-                    _fields_ = [
-                        ("x", c_ubyte),
-                    ]
+    @subTests("use_struct_util", [False, True])
+    def test_zero_align_with_fields(self, use_struct_util):
+        for endian in ('native', 'little', 'big'):
+            with self.subTest(endian=endian):
+                if use_struct_util:
+                    @struct_util(endian=endian, align=0)
+                    class MyStructure:
+                        x: c_ubyte
+                else:
+                    base = get_struct_base(endian)
+                    class MyStructure(base):
+                        _align_ = 0
+                        _fields_ = [
+                            ("x", c_ubyte),
+                        ]
 
                 self.assertEqual(alignment(MyStructure), 1)
                 self.assertEqual(alignment(MyStructure()), 1)
 
-    def test_oversized_structure(self):
+    @subTests("use_struct_util", [False, True])
+    def test_oversized_structure(self, use_struct_util):
         data = bytearray(b"\0" * 8)
-        for base in (LittleEndianStructure, BigEndianStructure):
-            class SomeBoolsTooBig(base):
-                _align_ = 8
-                _fields_ = [
-                    ("bool1", c_ubyte),
-                    ("bool2", c_ubyte),
-                    ("bool3", c_ubyte),
-                ]
+        for endian in ('little', 'big'):
+            if use_struct_util:
+                @struct_util(endian=endian, align=8)
+                class SomeBoolsTooBig:
+                    bool1: c_ubyte
+                    bool2: c_ubyte
+                    bool3: c_ubyte
+            else:
+                base = get_struct_base(endian)
+                class SomeBoolsTooBig(base):
+                    _align_ = 8
+                    _fields_ = [
+                        ("bool1", c_ubyte),
+                        ("bool2", c_ubyte),
+                        ("bool3", c_ubyte),
+                    ]
             self.check_struct(SomeBoolsTooBig)
-            class Main(base):
-                _fields_ = [
-                    ("y", SomeBoolsTooBig),
-                    ("z", c_uint32),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian)
+                class Main:
+                    y: SomeBoolsTooBig
+                    z: c_uint32
+            else:
+                class Main(base):
+                    _fields_ = [
+                        ("y", SomeBoolsTooBig),
+                        ("z", c_uint32),
+                    ]
             self.check_struct(Main)
             with self.assertRaises(ValueError) as ctx:
                 Main.from_buffer(data)
@@ -128,31 +214,49 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
                     'Buffer size too small (4 instead of at least 8 bytes)'
                 )
 
-    def test_aligned_subclasses(self):
-        for base, e in (
-            (LittleEndianStructure, "<"),
-            (BigEndianStructure, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_subclasses(self, use_struct_util):
+        for endian, e in (
+            ('little', "<"),
+            ('big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
-            class UnalignedSub(base):
-                x: c_uint32
-                _fields_ = [
-                    ("x", c_uint32),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian)
+                class UnalignedSub:
+                    x: c_uint32
+            else:
+                base = get_struct_base(endian)
+                class UnalignedSub(base):
+                    x: c_uint32
+                    _fields_ = [
+                        ("x", c_uint32),
+                    ]
             self.check_struct(UnalignedSub)
 
-            class AlignedStruct(UnalignedSub):
-                _align_ = 8
-                _fields_ = [
-                    ("y", c_uint32),
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian, align=8)
+                class AlignedStruct(UnalignedSub):
+                    y: c_uint32
+            else:
+                class AlignedStruct(UnalignedSub):
+                    _align_ = 8
+                    _fields_ = [
+                        ("y", c_uint32),
+                    ]
             self.check_struct(AlignedStruct)
 
-            class Main(base):
-                _fields_ = [
-                    ("a", c_uint32),
-                    ("b", AlignedStruct)
-                ]
+            if use_struct_util:
+                @struct_util(endian=endian)
+                class Main:
+                    a: c_uint32
+                    b: AlignedStruct
+            else:
+                class Main(base):
+                    _fields_ = [
+                        ("a", c_uint32),
+                        ("b", AlignedStruct)
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -166,12 +270,14 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(Main.b.offset, 8)
             self.assertEqual(Main.b.size, 8)
 
-    def test_aligned_union(self):
-        for sbase, ubase, e in (
-            (LittleEndianStructure, LittleEndianUnion, "<"),
-            (BigEndianStructure, BigEndianUnion, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_union(self, use_struct_util):
+        for sendian, uendian, e in (
+            ('little', 'little', "<"),
+            ('big', 'big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
+            ubase = get_union_base(uendian)
             class AlignedUnion(ubase):
                 _align_ = 8
                 _fields_ = [
@@ -180,11 +286,18 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
                 ]
             self.check_union(AlignedUnion)
 
-            class Main(sbase):
-                _fields_ = [
-                    ("first", c_uint32),
-                    ("union", AlignedUnion),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian)
+                class Main:
+                    first: c_uint32
+                    union: AlignedUnion
+            else:
+                sbase = get_struct_base(sendian)
+                class Main(sbase):
+                    _fields_ = [
+                        ("first", c_uint32),
+                        ("union", AlignedUnion),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -196,20 +309,29 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(alignment(main.union), 8)
             self.assertEqual(alignment(main), 8)
 
-    def test_aligned_struct_in_union(self):
-        for sbase, ubase, e in (
-            (LittleEndianStructure, LittleEndianUnion, "<"),
-            (BigEndianStructure, BigEndianUnion, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_struct_in_union(self, use_struct_util):
+        for sendian, uendian, e in (
+            ('little', 'little', "<"),
+            ('big', 'big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
-            class Sub(sbase):
-                _align_ = 8
-                _fields_ = [
-                    ("x", c_uint32),
-                    ("y", c_uint32),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian, align=8)
+                class Sub:
+                    x: c_uint32
+                    y: c_uint32
+            else:
+                sbase = get_struct_base(sendian)
+                class Sub(sbase):
+                    _align_ = 8
+                    _fields_ = [
+                        ("x", c_uint32),
+                        ("y", c_uint32),
+                    ]
             self.check_struct(Sub)
 
+            ubase = get_union_base(uendian)
             class MainUnion(ubase):
                 _fields_ = [
                     ("a", c_uint32),
@@ -217,11 +339,17 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
                 ]
             self.check_union(MainUnion)
 
-            class Main(sbase):
-                _fields_ = [
-                    ("first", c_uint32),
-                    ("union", MainUnion),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian)
+                class Main:
+                    first: c_uint32
+                    union: MainUnion
+            else:
+                class Main(sbase):
+                    _fields_ = [
+                        ("first", c_uint32),
+                        ("union", MainUnion),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -235,12 +363,14 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(main.union.b.x, 3)
             self.assertEqual(main.union.b.y, 4)
 
-    def test_smaller_aligned_subclassed_union(self):
-        for sbase, ubase, e in (
-            (LittleEndianStructure, LittleEndianUnion, "<"),
-            (BigEndianStructure, BigEndianUnion, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_smaller_aligned_subclassed_union(self, use_struct_util):
+        for sendian, uendian, e in (
+            ('little', 'little', "<"),
+            ('big', 'big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}H2xI", 1, 0xD60102D7))
+            ubase = get_union_base(uendian)
             class SubUnion(ubase):
                 _align_ = 2
                 _fields_ = [
@@ -255,11 +385,18 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
                 ]
             self.check_union(SubUnion)
 
-            class Main(sbase):
-                _fields_ = [
-                    ("first", c_uint16),
-                    ("union", MainUnion),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian)
+                class Main:
+                    first: c_uint16
+                    union: MainUnion
+            else:
+                sbase = get_struct_base(sendian)
+                class Main(sbase):
+                    _fields_ = [
+                        ("first", c_uint16),
+                        ("union", MainUnion),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)
@@ -273,11 +410,12 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(Main.first.size, 2)
 
     def test_larger_aligned_subclassed_union(self):
-        for ubase, e in (
-            (LittleEndianUnion, "<"),
-            (BigEndianUnion, ">"),
+        for uendian, e in (
+            ('little', "<"),
+            ('big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}I4x", 0xD60102D6))
+            ubase = get_union_base(uendian)
             class SubUnion(ubase):
                 _align_ = 8
                 _fields_ = [
@@ -299,29 +437,44 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
             self.assertEqual(main.unsigned, 0xD6)
             self.assertEqual(main.signed, -42)
 
-    def test_aligned_packed_structures(self):
-        for sbase, e in (
-            (LittleEndianStructure, "<"),
-            (BigEndianStructure, ">"),
+    @subTests("use_struct_util", [False, True])
+    def test_aligned_packed_structures(self, use_struct_util):
+        for sendian, e in (
+            ('little', "<"),
+            ('big', ">"),
         ):
             data = bytearray(struct.pack(f"{e}B2H4xB", 1, 2, 3, 4))
 
-            class Inner(sbase):
-                _align_ = 8
-                _fields_ = [
-                    ("x", c_uint16),
-                    ("y", c_uint16),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian, align=8)
+                class Inner:
+                    x: c_uint16
+                    y: c_uint16
+            else:
+                sbase = get_struct_base(sendian)
+                class Inner(sbase):
+                    _align_ = 8
+                    _fields_ = [
+                        ("x", c_uint16),
+                        ("y", c_uint16),
+                    ]
             self.check_struct(Inner)
 
-            class Main(sbase):
-                _pack_ = 1
-                _layout_ = "ms"
-                _fields_ = [
-                    ("a", c_ubyte),
-                    ("b", Inner),
-                    ("c", c_ubyte),
-                ]
+            if use_struct_util:
+                @struct_util(endian=sendian, pack=1, layout="ms")
+                class Main:
+                    a: c_ubyte
+                    b: Inner
+                    c: c_ubyte
+            else:
+                class Main(sbase):
+                    _pack_ = 1
+                    _layout_ = "ms"
+                    _fields_ = [
+                        ("a", c_ubyte),
+                        ("b", Inner),
+                        ("c", c_ubyte),
+                    ]
             self.check_struct(Main)
 
             main = Main.from_buffer(data)