del _make_unary_op
-def _template_to_ast(template):
+def _template_to_ast_constructor(template):
+ """Convert a `template` instance to a non-literal AST."""
+ args = []
+ for part in template:
+ match part:
+ case str():
+ args.append(ast.Constant(value=part))
+ case _:
+ interp = ast.Call(
+ func=ast.Name(id="Interpolation"),
+ args=[
+ ast.Constant(value=part.value),
+ ast.Constant(value=part.expression),
+ ast.Constant(value=part.conversion),
+ ast.Constant(value=part.format_spec),
+ ]
+ )
+ args.append(interp)
+ return ast.Call(func=ast.Name(id="Template"), args=args, keywords=[])
+
+
+def _template_to_ast_literal(template, parsed):
+ """Convert a `template` instance to a t-string literal AST."""
values = []
+ interp_count = 0
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
- ),
+ value=parsed[interp_count],
+ conversion=ord(part.conversion) if part.conversion else -1,
+ format_spec=ast.Constant(value=part.format_spec)
+ if part.format_spec
+ else None,
)
values.append(interp)
+ interp_count += 1
return ast.TemplateStr(values=values)
+def _template_to_ast(template):
+ """Make a best-effort conversion of a `template` instance to an AST."""
+ # gh-138558: Not all Template instances can be represented as t-string
+ # literals. Return the most accurate AST we can. See issue for details.
+
+ # If any expr is empty or whitespace only, we cannot convert to a literal.
+ if any(part.expression.strip() == "" for part in template.interpolations):
+ return _template_to_ast_constructor(template)
+
+ try:
+ # Wrap in parens to allow whitespace inside interpolation curly braces
+ parsed = tuple(
+ ast.parse(f"({part.expression})", mode="eval").body
+ for part in template.interpolations
+ )
+ except SyntaxError:
+ return _template_to_ast_constructor(template)
+
+ return _template_to_ast_literal(template, parsed)
+
+
class _StringifierDict(dict):
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
super().__init__(namespace)
import functools
import itertools
import pickle
-from string.templatelib import Template
+from string.templatelib import Template, Interpolation
import typing
import unittest
from annotationlib import (
a: t"a{b}c{d}e{f}g",
b: t"{a:{1}}",
c: t"{a | b * c}",
+ gh138558: t"{ 0}",
): pass
annos = get_annotations(f, format=Format.STRING)
# interpolations in the format spec are eagerly evaluated so we can't recover the source
"b": "t'{a:1}'",
"c": "t'{a | b * c}'",
+ "gh138558": "t'{ 0}'",
})
def g(
self.assertEqual(type_repr("1"), "'1'")
self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE))
self.assertEqual(type_repr(MyClass()), "my repr")
+ # gh138558 tests
+ self.assertEqual(type_repr(t'''{ 0
+ & 1
+ | 2
+ }'''), 't"""{ 0\n & 1\n | 2}"""')
+ self.assertEqual(
+ type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
+ )
+ self.assertEqual(
+ type_repr(Template("hi", Interpolation(42))),
+ "Template('hi', Interpolation(42, '', None, ''))",
+ )
+ self.assertEqual(
+ type_repr(Template("hi", Interpolation(42, " "))),
+ "Template('hi', Interpolation(42, ' ', None, ''))",
+ )
+ # gh138558: perhaps in the future, we can improve this behavior:
+ self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'")
class TestAnnotationsToString(unittest.TestCase):