From: Victor Stinner Date: Sun, 19 Jul 2026 10:07:43 +0000 (+0200) Subject: gh-104533: Fix `@ctypes.util.struct` with endian (#154038) X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;h=e26005199b17792b47bed10096eaface7fc60b40;p=thirdparty%2FPython%2Fcpython.git 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. --- diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index e8db8c578bb3..a73422598b2c 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 50b4d729b9db..d58d92a19eb3 100644 --- a/Lib/test/test_ctypes/test_aligned_structures.py +++ b/Lib/test/test_ctypes/test_aligned_structures.py @@ -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)