Skip to content

Commit bfe761c

Browse files
gh-157056: Fix STRING format for comprehension and lambda annotations
get_annotations(..., format=Format.STRING) raised ValueError on dict comprehension annotations because fake-globals iteration cannot unpack pair targets. Recover the annotation text from source in that case. Lambda and generator-expression annotations are syntax, so they were stringified with repr() and leaked a memory address. Prefer the source text when available, and use a stable type_repr() otherwise.
1 parent 3c7ae4c commit bfe761c

1 file changed

Lines changed: 98 additions & 5 deletions

File tree

‎Lib/annotationlib.py‎

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -750,13 +750,20 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
750750
argdefs=annotate.__defaults__,
751751
kwdefaults=annotate.__kwdefaults__,
752752
)
753-
annos = func(Format.VALUE_WITH_FAKE_GLOBALS)
753+
try:
754+
annos = func(Format.VALUE_WITH_FAKE_GLOBALS)
755+
except ValueError:
756+
# Dict comprehensions such as `{k: v for k, v in items}` unpack
757+
# each iterated element. Fake-globals iteration yields a single
758+
# starred stringifier, so unpacking raises ValueError. Recover
759+
# the original annotation text from source when we can.
760+
sourced = _string_annotations_from_source(owner)
761+
if sourced is not None:
762+
return sourced
763+
raise
754764
if _is_evaluate:
755765
return _stringify_single(annos)
756-
return {
757-
key: _stringify_single(val)
758-
for key, val in annos.items()
759-
}
766+
return _stringify_annotation_dict(annos, owner)
760767
elif format == Format.FORWARDREF:
761768
# FORWARDREF is implemented similarly to STRING, but there are two changes,
762769
# at the beginning and the end of the process.
@@ -878,6 +885,81 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat
878885
return tuple(new_closure), cell_dict
879886

880887

888+
def _string_annotations_from_source(obj):
889+
"""Best-effort STRING annotations reconstructed from *obj*'s source AST.
890+
891+
Used when fake-globals evaluation cannot stringify an annotation (dict
892+
comprehensions, lambdas, generator expressions). Returns None if source
893+
is unavailable. inspect is imported lazily because it imports this module.
894+
"""
895+
if obj is None:
896+
return None
897+
try:
898+
import inspect
899+
import textwrap
900+
source = inspect.getsource(obj)
901+
except (OSError, TypeError, RecursionError):
902+
return None
903+
source = textwrap.dedent(source)
904+
try:
905+
tree = ast.parse(source)
906+
except SyntaxError:
907+
return None
908+
if not tree.body:
909+
return None
910+
node = tree.body[0]
911+
result = {}
912+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
913+
for arg in (
914+
*node.args.posonlyargs,
915+
*node.args.args,
916+
*node.args.kwonlyargs,
917+
):
918+
if arg.annotation is not None:
919+
result[arg.arg] = ast.unparse(arg.annotation)
920+
if node.args.vararg is not None and node.args.vararg.annotation is not None:
921+
result[node.args.vararg.arg] = ast.unparse(node.args.vararg.annotation)
922+
if node.args.kwarg is not None and node.args.kwarg.annotation is not None:
923+
result[node.args.kwarg.arg] = ast.unparse(node.args.kwarg.annotation)
924+
if node.returns is not None:
925+
result["return"] = ast.unparse(node.returns)
926+
return result
927+
if isinstance(node, ast.ClassDef):
928+
for stmt in node.body:
929+
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
930+
result[stmt.target.id] = ast.unparse(stmt.annotation)
931+
return result
932+
return None
933+
934+
935+
def _is_runtime_constructed(value):
936+
"""True if *value* was created at annotation-eval time and has no AST.
937+
938+
Lambdas and generator expressions are syntax, not name lookups, so the
939+
fake-globals stringifier never sees them. Their repr() embeds a memory
940+
address and is not a valid annotation string.
941+
"""
942+
return isinstance(value, (
943+
types.FunctionType,
944+
types.BuiltinFunctionType,
945+
types.MethodType,
946+
types.GeneratorType,
947+
types.AsyncGeneratorType,
948+
types.CoroutineType,
949+
))
950+
951+
952+
def _stringify_annotation_dict(annos, owner):
953+
sourced = _string_annotations_from_source(owner)
954+
result = {}
955+
for key, val in annos.items():
956+
if sourced is not None and key in sourced and _is_runtime_constructed(val):
957+
result[key] = sourced[key]
958+
else:
959+
result[key] = _stringify_single(val)
960+
return result
961+
962+
881963
def _stringify_single(anno):
882964
if anno is ...:
883965
return "..."
@@ -886,6 +968,10 @@ def _stringify_single(anno):
886968
return anno
887969
elif isinstance(anno, _Template):
888970
return ast.unparse(_template_to_ast(anno))
971+
elif _is_runtime_constructed(anno):
972+
# Lambdas and generator expressions are syntax, not name lookups.
973+
# repr() embeds a memory address; type_repr() is stable.
974+
return type_repr(anno)
889975
else:
890976
return repr(anno)
891977

@@ -1093,6 +1179,13 @@ def type_repr(value):
10931179
if value.__module__ == "builtins":
10941180
return value.__qualname__
10951181
return f"{value.__module__}.{value.__qualname__}"
1182+
elif isinstance(value, (
1183+
types.GeneratorType,
1184+
types.AsyncGeneratorType,
1185+
types.CoroutineType,
1186+
)):
1187+
# repr() of these objects embeds a memory address.
1188+
return value.__qualname__
10961189
elif isinstance(value, _Template):
10971190
tree = _template_to_ast(value)
10981191
return ast.unparse(tree)

0 commit comments

Comments
 (0)