Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ducktools/classbuilder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions src/ducktools/classbuilder/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions src/ducktools/classbuilder/annotations.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]]: ...

Expand Down
21 changes: 19 additions & 2 deletions tests/py314_tests/test_init_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
[
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down