I’m not sure I like this in its current form.
Calling get_annotations(obj, format=Format.AST) gives a dict[str, ast.expr] but your create_annotate_from_asts function requires each expression also have an associated Mapping but it’s not clear where this is supposed to come from or what exactly it should be?
create_annotate_from_asts(annotations: dict[str, tuple[ast.expr, Mapping[str, object]]]) -> Callable[[Format], dict[str, object]]
Presumably it’s related to the context in which names in the ast expression should be evaluated?
I’m also not sure how I would be intended to use this if I need to add objects that have not been obtained through get_annotations to the new annotations? Things like the return None value for dataclasses’ __init__ or a dynamically created TypedDict for an as_dict method[1].
I’m actually curious also to how memory usage would compare to capturing the original source text of the annotations. I assume that this wasn’t done originally for memory use reasons too, but it does have the unfortunate side effect that call_annotate_function(cls.__annotate__, format=Format.STRING) is significantly[2] slower than call_annotate_function(cls.__annotate__, format=Format.VALUE).
Speaking of STRING, I don’t like that this implementation means that the AST format can construct a more accurate STRING than the STRING format itself.
There appears to be a bug in the reference implementation with how the AST is constructed in the reference as op in a BinOp should be an instance of the operation and not the class but with a little editing…
from annotationlib import get_annotations, Format
import ast
class Example:
a: 1 | 2 | 3
a_anno = get_annotations(Example, format=Format.AST)['a']
# ast.unparse will fail as this is currently the class and not an instance
print(f"{a_anno.op = }")
# Editing in post
a_anno.op = ast.BitOr()
a_anno.left.op = ast.BitOr()
print(ast.unparse(a_anno))
print(get_annotations(Example, format=Format.STRING)['a'])
Output:
a_anno.op = <class 'ast.BitOr'>
1 | 2 | 3
3