Skip to content

Commit 158dbbb

Browse files
gh-148680: Replace internal names with type_reprs of objects in string representations of ForwardRef (#148682)
Co-authored-by: Shamil <ashm.tech@proton.me>
1 parent 9633c52 commit 158dbbb

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

Lib/annotationlib.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Format(enum.IntEnum):
4747
"__cell__",
4848
"__owner__",
4949
"__stringifier_dict__",
50+
"__resolved_str_cache__",
5051
)
5152

5253

@@ -94,6 +95,7 @@ def __init__(
9495
# value later.
9596
self.__code__ = None
9697
self.__ast_node__ = None
98+
self.__resolved_str_cache__ = None
9799

98100
def __init_subclass__(cls, /, *args, **kwds):
99101
raise TypeError("Cannot subclass ForwardRef")
@@ -113,7 +115,7 @@ def evaluate(
113115
"""
114116
match format:
115117
case Format.STRING:
116-
return self.__forward_arg__
118+
return self.__resolved_str__
117119
case Format.VALUE:
118120
is_forwardref_format = False
119121
case Format.FORWARDREF:
@@ -258,6 +260,24 @@ def __forward_arg__(self):
258260
"Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
259261
)
260262

263+
@property
264+
def __resolved_str__(self):
265+
# __forward_arg__ with any names from __extra_names__ replaced
266+
# with the type_repr of the value they represent
267+
if self.__resolved_str_cache__ is None:
268+
resolved_str = self.__forward_arg__
269+
names = self.__extra_names__
270+
271+
if names:
272+
visitor = _ExtraNameFixer(names)
273+
ast_expr = ast.parse(resolved_str, mode="eval").body
274+
node = visitor.visit(ast_expr)
275+
resolved_str = ast.unparse(node)
276+
277+
self.__resolved_str_cache__ = resolved_str
278+
279+
return self.__resolved_str_cache__
280+
261281
@property
262282
def __forward_code__(self):
263283
if self.__code__ is not None:
@@ -321,7 +341,7 @@ def __repr__(self):
321341
extra.append(", is_class=True")
322342
if self.__owner__ is not None:
323343
extra.append(f", owner={self.__owner__!r}")
324-
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
344+
return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})"
325345

326346

327347
_Template = type(t"")
@@ -357,6 +377,7 @@ def __init__(
357377
self.__cell__ = cell
358378
self.__owner__ = owner
359379
self.__stringifier_dict__ = stringifier_dict
380+
self.__resolved_str_cache__ = None # Needed for ForwardRef
360381

361382
def __convert_to_ast(self, other):
362383
if isinstance(other, _Stringifier):
@@ -1163,3 +1184,14 @@ def _get_dunder_annotations(obj):
11631184
if not isinstance(ann, dict):
11641185
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
11651186
return ann
1187+
1188+
1189+
class _ExtraNameFixer(ast.NodeTransformer):
1190+
"""Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
1191+
def __init__(self, extra_names):
1192+
self.extra_names = extra_names
1193+
1194+
def visit_Name(self, node: ast.Name):
1195+
if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
1196+
node = ast.Name(id=type_repr(new_name))
1197+
return node

Lib/test/test_annotationlib.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,15 @@ def test_forward_repr(self):
19611961
"typing.List[ForwardRef('int', owner='class')]",
19621962
)
19631963

1964+
def test_forward_repr_extra_names(self):
1965+
def f(a: undefined | str): ...
1966+
1967+
annos = get_annotations(f, format=Format.FORWARDREF)
1968+
1969+
self.assertRegex(
1970+
repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)"
1971+
)
1972+
19641973
def test_forward_recursion_actually(self):
19651974
def namespace1():
19661975
a = ForwardRef("A")
@@ -2037,6 +2046,17 @@ def test_evaluate_string_format(self):
20372046
fr = ForwardRef("set[Any]")
20382047
self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]")
20392048

2049+
def test_evaluate_string_format_extra_names(self):
2050+
# Test that internal extra_names are replaced when evaluating as strings
2051+
def f(a: unknown | str | int | list[str] | tuple[int, ...]): ...
2052+
2053+
fr = get_annotations(f, format=Format.FORWARDREF)['a']
2054+
# Test the cache is not populated before access
2055+
self.assertIsNone(fr.__resolved_str_cache__)
2056+
2057+
self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]")
2058+
self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]")
2059+
20402060
def test_evaluate_forwardref_format(self):
20412061
fr = ForwardRef("undef")
20422062
evaluated = fr.evaluate(format=Format.FORWARDREF)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``ForwardRef`` objects that contain internal names to represent known objects now show the ``type_repr`` of the known object rather than the internal ``__annotationlib_name_x__`` name when evaluated as strings.

0 commit comments

Comments
 (0)