gh-104533: Fix `@ctypes.util.struct` with endian (#154038)

vstinner <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/e26005199b17792b47bed10096eaface7fc60b40
commit: e26005199b17792b47bed10096eaface7fc60b40
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-07-19T12:07:43+02:00
summary:

gh-104533: Fix `@ctypes.util.struct` with endian (#154038)

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.

files:
M Lib/ctypes/util.py
M Lib/test/test_ctypes/test_aligned_structures.py

diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py
index e8db8c578bb360..a73422598b2cd9 100644
--- a/Lib/ctypes/util.py
+++ b/Lib/ctypes/util.py
@@ -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
diff --git a/Lib/test/test_ctypes/test_aligned_structures.py b/Lib/test/test_ctypes/test_aligned_structures.py
index 50b4d729b9db8a..d58d92a19eb33c 100644
--- a/Lib/test/test_ctypes/test_aligned_structures.py
+++ b/Lib/test/test_ctypes/test_aligned_structures.py
@@ -3,29 +3,64 @@
     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 Main(base):
             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 Main(base):
             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 Main(base):
                     '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 Main(base):
             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 AlignedUnion(ubase):
                 ]
             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 Main(sbase):
             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 MainUnion(ubase):
                 ]
             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 Main(sbase):
             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 MainUnion(SubUnion):
                 ]
             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 Main(sbase):
             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 Main(SubUnion):
             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)

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.