"__cell__",
"__owner__",
"__stringifier_dict__",
+ "__resolved_str_cache__",
)
# value later.
self.__code__ = None
self.__ast_node__ = None
+ self.__resolved_str_cache__ = None
def __init_subclass__(cls, /, *args, **kwds):
raise TypeError("Cannot subclass ForwardRef")
"""
match format:
case Format.STRING:
- return self.__forward_arg__
+ return self.__resolved_str__
case Format.VALUE:
is_forwardref_format = False
case Format.FORWARDREF:
"Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
)
+ @property
+ def __resolved_str__(self):
+ # __forward_arg__ with any names from __extra_names__ replaced
+ # with the type_repr of the value they represent
+ if self.__resolved_str_cache__ is None:
+ resolved_str = self.__forward_arg__
+ names = self.__extra_names__
+
+ if names:
+ visitor = _ExtraNameFixer(names)
+ ast_expr = ast.parse(resolved_str, mode="eval").body
+ node = visitor.visit(ast_expr)
+ resolved_str = ast.unparse(node)
+
+ self.__resolved_str_cache__ = resolved_str
+
+ return self.__resolved_str_cache__
+
@property
def __forward_code__(self):
if self.__code__ is not None:
extra.append(", is_class=True")
if self.__owner__ is not None:
extra.append(f", owner={self.__owner__!r}")
- return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
+ return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})"
_Template = type(t"")
self.__cell__ = cell
self.__owner__ = owner
self.__stringifier_dict__ = stringifier_dict
+ self.__resolved_str_cache__ = None # Needed for ForwardRef
def __convert_to_ast(self, other):
if isinstance(other, _Stringifier):
if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
return ann
+
+
+class _ExtraNameFixer(ast.NodeTransformer):
+ """Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
+ def __init__(self, extra_names):
+ self.extra_names = extra_names
+
+ def visit_Name(self, node: ast.Name):
+ if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
+ node = ast.Name(id=type_repr(new_name))
+ return node
"typing.List[ForwardRef('int', owner='class')]",
)
+ def test_forward_repr_extra_names(self):
+ def f(a: undefined | str): ...
+
+ annos = get_annotations(f, format=Format.FORWARDREF)
+
+ self.assertRegex(
+ repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)"
+ )
+
def test_forward_recursion_actually(self):
def namespace1():
a = ForwardRef("A")
fr = ForwardRef("set[Any]")
self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]")
+ def test_evaluate_string_format_extra_names(self):
+ # Test that internal extra_names are replaced when evaluating as strings
+ def f(a: unknown | str | int | list[str] | tuple[int, ...]): ...
+
+ fr = get_annotations(f, format=Format.FORWARDREF)['a']
+ # Test the cache is not populated before access
+ self.assertIsNone(fr.__resolved_str_cache__)
+
+ self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]")
+ self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]")
+
def test_evaluate_forwardref_format(self):
fr = ForwardRef("undef")
evaluated = fr.evaluate(format=Format.FORWARDREF)