From 83674483563c83550c87311adb557f8565916ce0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jul 2026 12:05:22 +0200 Subject: [PATCH] gh-104533: Fix `@ctypes.util.struct` with string annotations (#154040) Fix `@ctypes.util.struct` on structures declared in a module which uses `from __future__ import annotations`. The decorator now calls get_annotations() with eval_str=True. --- Lib/ctypes/util.py | 3 ++- Lib/test/test_ctypes/struct_str_ann.py | 15 +++++++++++++++ Lib/test/test_ctypes/test_structures.py | 13 ++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 Lib/test/test_ctypes/struct_str_ann.py diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index c65fc3032fdf..e8db8c578bb3 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -512,7 +512,8 @@ def _process_struct(decorated_class, /, *, align, layout, endian, pack): fields.extend(decorated_class._fields_) anonymous.extend(decorated_class._anonymous_) - for name, hint in annotationlib.get_annotations(decorated_class).items(): + annotations = annotationlib.get_annotations(decorated_class, eval_str=True) + for name, hint in annotations.items(): if get_origin(hint) is ClassVar: continue diff --git a/Lib/test/test_ctypes/struct_str_ann.py b/Lib/test/test_ctypes/struct_str_ann.py new file mode 100644 index 000000000000..7a924825dd81 --- /dev/null +++ b/Lib/test/test_ctypes/struct_str_ann.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from ctypes.util import struct +from ctypes import c_int + +class TestAnn: + x: c_int + +# Check that "from __future__ import annotations" works as expected +if not isinstance(TestAnn.__annotations__['x'], str): + raise Exception("annotations must be strings") + +@struct +class Point: + x: c_int + y: c_int diff --git a/Lib/test/test_ctypes/test_structures.py b/Lib/test/test_ctypes/test_structures.py index 4dc1ea867c07..04c3682e8308 100644 --- a/Lib/test/test_ctypes/test_structures.py +++ b/Lib/test/test_ctypes/test_structures.py @@ -88,13 +88,14 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin): self.assertEqual(sizeof(X), min(8, longlong_align) + longlong_size) self.assertEqual(X.b.offset, min(8, longlong_align)) - with self.assertRaises(ValueError): - if use_struct_util: + if use_struct_util: + with self.assertRaises(NameError): @struct_util(pack=-1, layout='ms') class X: a: "b" b: "q" - else: + else: + with self.assertRaises(ValueError): class X(Structure): _fields_ = [("a", "b"), ("b", "q")] _pack_ = -1 @@ -954,6 +955,12 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin): self.assertEqual(Foo.__name__, "Foo") + def test_string_annotations(self): + from test.test_ctypes import struct_str_ann + Point = struct_str_ann.Point + fields = [['x', c_int], ['y', c_int]] + self.assertEqual(Point._fields_, fields) + if __name__ == '__main__': unittest.main() -- 2.47.3