return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
+_Template = type(t"")
+
+
class _Stringifier:
# Must match the slots on ForwardRef, so we can turn an instance of one into an
# instance of the other in place.
if isinstance(other.__ast_node__, str):
return ast.Name(id=other.__ast_node__), other.__extra_names__
return other.__ast_node__, other.__extra_names__
+ elif type(other) is _Template:
+ return _template_to_ast(other), None
elif (
# In STRING format we don't bother with the create_unique_name() dance;
# it's better to emit the repr() of the object instead of an opaque name.
del _make_unary_op
+def _template_to_ast(template):
+ values = []
+ for part in template:
+ match part:
+ case str():
+ values.append(ast.Constant(value=part))
+ # Interpolation, but we don't want to import the string module
+ case _:
+ interp = ast.Interpolation(
+ str=part.expression,
+ value=ast.parse(part.expression),
+ conversion=(
+ ord(part.conversion)
+ if part.conversion is not None
+ else -1
+ ),
+ format_spec=(
+ ast.Constant(value=part.format_spec)
+ if part.format_spec != ""
+ else None
+ ),
+ )
+ values.append(interp)
+ return ast.TemplateStr(values=values)
+
+
class _StringifierDict(dict):
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
super().__init__(namespace)
# We have to handle str specially to support PEP 563 stringified annotations.
elif isinstance(anno, str):
return anno
+ elif isinstance(anno, _Template):
+ return ast.unparse(_template_to_ast(anno))
else:
return repr(anno)
if value.__module__ == "builtins":
return value.__qualname__
return f"{value.__module__}.{value.__qualname__}"
+ elif isinstance(value, _Template):
+ tree = _template_to_ast(value)
+ return ast.unparse(tree)
if value is ...:
return "..."
return repr(value)
"encoded_modules/module_iso_8859_1.py",
"encoded_modules/module_koi8_r.py",
# SyntaxError because of t-strings
- "test_tstring.py",
+ "test_annotationlib.py",
"test_string/test_templatelib.py",
+ "test_tstring.py",
# New grammar constructions may not yet be recognized by Ruff,
# and tests re-use the same names as only the grammar is being checked.
"test_grammar.py",
import functools
import itertools
import pickle
+from string.templatelib import Interpolation, Template
import typing
import unittest
from annotationlib import (
},
)
+ def test_template_str(self):
+ def f(
+ x: t"{a}",
+ y: list[t"{a}"],
+ z: t"{a:b} {c!r} {d!s:t}",
+ a: t"a{b}c{d}e{f}g",
+ b: t"{a:{1}}",
+ c: t"{a | b * c}",
+ ): pass
+
+ annos = get_annotations(f, format=Format.STRING)
+ self.assertEqual(annos, {
+ "x": "t'{a}'",
+ "y": "list[t'{a}']",
+ "z": "t'{a:b} {c!r} {d!s:t}'",
+ "a": "t'a{b}c{d}e{f}g'",
+ # interpolations in the format spec are eagerly evaluated so we can't recover the source
+ "b": "t'{a:1}'",
+ "c": "t'{a | b * c}'",
+ })
+
+ def g(
+ x: t"{a}",
+ ): ...
+
+ annos = get_annotations(g, format=Format.FORWARDREF)
+ templ = annos["x"]
+ # Template and Interpolation don't have __eq__ so we have to compare manually
+ self.assertIsInstance(templ, Template)
+ self.assertEqual(templ.strings, ("", ""))
+ self.assertEqual(len(templ.interpolations), 1)
+ interp = templ.interpolations[0]
+ self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g))
+ self.assertEqual(interp.expression, "a")
+ self.assertIsNone(interp.conversion)
+ self.assertEqual(interp.format_spec, "")
+
def test_getitem(self):
def f(x: undef1[str, undef2]):
pass