diff --git a/src/ducktools/classbuilder/__init__.py b/src/ducktools/classbuilder/__init__.py index 84fe030..06ae0bc 100644 --- a/src/ducktools/classbuilder/__init__.py +++ b/src/ducktools/classbuilder/__init__.py @@ -215,7 +215,8 @@ 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) + 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 3b90e50..d672312 100644 --- a/src/ducktools/classbuilder/annotations.py +++ b/src/ducktools/classbuilder/annotations.py @@ -83,14 +83,15 @@ 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 # Construct an annotation function from __annotations__ - def annotate_func(format, /): + def __annotate__(format, /): match format: case Format.VALUE | Format.FORWARDREF: return { @@ -99,18 +100,22 @@ 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: + # 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/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]]: ... diff --git a/tests/py314_tests/test_init_signature.py b/tests/py314_tests/test_init_signature.py index b77725c..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"], [ @@ -66,12 +74,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 +109,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