From 36d96e74471aff5912463f5e9882e79d2f4636d0 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Fri, 15 Aug 2025 13:24:43 +0100 Subject: [PATCH 1/3] Better string annotations results from generated annotate methods --- src/ducktools/classbuilder/__init__.py | 2 +- src/ducktools/classbuilder/annotations.py | 16 ++++++++++------ tests/py314_tests/test_init_signature.py | 13 +++++++++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/ducktools/classbuilder/__init__.py b/src/ducktools/classbuilder/__init__.py index 84fe030..a867f74 100644 --- a/src/ducktools/classbuilder/__init__.py +++ b/src/ducktools/classbuilder/__init__.py @@ -215,7 +215,7 @@ def __get__(self, inst, cls): if "__annotations__" in gen_cls.__dict__: method.__annotations__ = gen.annotations else: - anno_func = make_annotate_func(gen.annotations) + anno_func = make_annotate_func(gen_cls, gen.annotations) method.__annotate__ = anno_func else: method.__annotations__ = gen.annotations diff --git a/src/ducktools/classbuilder/annotations.py b/src/ducktools/classbuilder/annotations.py index 3b90e50..5b38ab9 100644 --- a/src/ducktools/classbuilder/annotations.py +++ b/src/ducktools/classbuilder/annotations.py @@ -83,9 +83,10 @@ def get_ns_annotations(ns): return annotations -def make_annotate_func(annos): +def make_annotate_func(cls, annos): # Only used in 3.14 or later so no sys.version_info gate + get_annotations = _lazy_annotationlib.get_annotations type_repr = _lazy_annotationlib.type_repr Format = _lazy_annotationlib.Format ForwardRef = _lazy_annotationlib.ForwardRef @@ -99,13 +100,16 @@ def annotate_func(format, /): for k, v in annos.items() } case Format.STRING: + cls_annotations = {} + for base in reversed(cls.__mro__): + cls_annotations.update( + get_annotations(base, format=Format.STRING) + ) string_annos = {} for k, v in annos.items(): - if isinstance(v, str): - string_annos[k] = v - elif isinstance(v, ForwardRef): - string_annos[k] = v.evaluate(format=Format.STRING) - else: + try: + string_annos[k] = cls_annotations[k] + except KeyError: string_annos[k] = type_repr(v) return string_annos case _: diff --git a/tests/py314_tests/test_init_signature.py b/tests/py314_tests/test_init_signature.py index b77725c..32c36cf 100644 --- a/tests/py314_tests/test_init_signature.py +++ b/tests/py314_tests/test_init_signature.py @@ -66,12 +66,12 @@ class Example: [ (Format.VALUE, {"return": None, "x": int, "y": type_str}), (Format.FORWARDREF, {"return": None, "x": int, "y": type_str}), - (Format.STRING, {"return": "None", "x": "int", "y": "type_str"}), + (Format.STRING, {"return": "None", "x": "assign_int", "y": "type_str"}), ] ) def test_alias_defined_annotations(format, expected): # Test the behaviour of type aliases and regular types - # Type Alias names should be kept while regular assignments will be lost + # Both names should be kept in string annotations @prefab class Example: @@ -101,6 +101,15 @@ class Example(Prefab): assert annos == expected +def test_contained_string_annotation(): + class Example(Prefab): + x: list[undefined] + + annos = get_annotations(Example.__init__, format=Format.STRING) + + assert annos == {"return": "None", "x": "list[undefined]"} + + def test_forwardref_raises(): # Should still raise a NameError with VALUE annotations @prefab From d59aca858c3902276b66102c91af13c2752261dc Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Fri, 15 Aug 2025 13:34:19 +0100 Subject: [PATCH 2/3] Fix up the qualname of the annotate function --- src/ducktools/classbuilder/__init__.py | 1 + src/ducktools/classbuilder/annotations.py | 5 +++-- tests/py314_tests/test_init_signature.py | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ducktools/classbuilder/__init__.py b/src/ducktools/classbuilder/__init__.py index a867f74..06ae0bc 100644 --- a/src/ducktools/classbuilder/__init__.py +++ b/src/ducktools/classbuilder/__init__.py @@ -216,6 +216,7 @@ def __get__(self, inst, cls): method.__annotations__ = gen.annotations else: anno_func = make_annotate_func(gen_cls, gen.annotations) + anno_func.__qualname__ = f"{gen_cls.__qualname__}.{self.funcname}.__annotate__" method.__annotate__ = anno_func else: method.__annotations__ = gen.annotations diff --git a/src/ducktools/classbuilder/annotations.py b/src/ducktools/classbuilder/annotations.py index 5b38ab9..d672312 100644 --- a/src/ducktools/classbuilder/annotations.py +++ b/src/ducktools/classbuilder/annotations.py @@ -91,7 +91,7 @@ def make_annotate_func(cls, annos): Format = _lazy_annotationlib.Format ForwardRef = _lazy_annotationlib.ForwardRef # Construct an annotation function from __annotations__ - def annotate_func(format, /): + def __annotate__(format, /): match format: case Format.VALUE | Format.FORWARDREF: return { @@ -110,11 +110,12 @@ def annotate_func(format, /): try: string_annos[k] = cls_annotations[k] except KeyError: + # Likely a return value string_annos[k] = type_repr(v) return string_annos case _: raise NotImplementedError(format) - return annotate_func + return __annotate__ def is_classvar(hint): diff --git a/tests/py314_tests/test_init_signature.py b/tests/py314_tests/test_init_signature.py index 32c36cf..c131ad6 100644 --- a/tests/py314_tests/test_init_signature.py +++ b/tests/py314_tests/test_init_signature.py @@ -39,6 +39,14 @@ class Example(Prefab): assert annos == expected +def test_annotate_qualname(): + @prefab + class Example: + x: str + + assert Example.__init__.__annotate__.__qualname__ == f"{Example.__qualname__}.__init__.__annotate__" + + @pytest.mark.parametrize( ["format", "expected"], [ From 5800e3c6ccd93350422325097552a7f723f8fbdb Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Fri, 15 Aug 2025 13:38:50 +0100 Subject: [PATCH 3/3] Fix stub file --- src/ducktools/classbuilder/annotations.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ducktools/classbuilder/annotations.pyi b/src/ducktools/classbuilder/annotations.pyi index 220cb8b..bf70fbd 100644 --- a/src/ducktools/classbuilder/annotations.pyi +++ b/src/ducktools/classbuilder/annotations.pyi @@ -13,6 +13,7 @@ def get_ns_annotations( ) -> dict[str, typing.Any]: ... def make_annotate_func( + cls: type, annos: dict[str, typing.Any] ) -> Callable[[int], dict[str, typing.Any]]: ...