Skip to content

Commit 6ec058a

Browse files
authored
gh-138558: Improve handling of Template annotations in annotationlib (#139072)
1 parent e8382e5 commit 6ec058a

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

Lib/annotationlib.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -560,32 +560,70 @@ def unary_op(self):
560560
del _make_unary_op
561561

562562

563-
def _template_to_ast(template):
563+
def _template_to_ast_constructor(template):
564+
"""Convert a `template` instance to a non-literal AST."""
565+
args = []
566+
for part in template:
567+
match part:
568+
case str():
569+
args.append(ast.Constant(value=part))
570+
case _:
571+
interp = ast.Call(
572+
func=ast.Name(id="Interpolation"),
573+
args=[
574+
ast.Constant(value=part.value),
575+
ast.Constant(value=part.expression),
576+
ast.Constant(value=part.conversion),
577+
ast.Constant(value=part.format_spec),
578+
]
579+
)
580+
args.append(interp)
581+
return ast.Call(func=ast.Name(id="Template"), args=args, keywords=[])
582+
583+
584+
def _template_to_ast_literal(template, parsed):
585+
"""Convert a `template` instance to a t-string literal AST."""
564586
values = []
587+
interp_count = 0
565588
for part in template:
566589
match part:
567590
case str():
568591
values.append(ast.Constant(value=part))
569-
# Interpolation, but we don't want to import the string module
570592
case _:
571593
interp = ast.Interpolation(
572594
str=part.expression,
573-
value=ast.parse(part.expression),
574-
conversion=(
575-
ord(part.conversion)
576-
if part.conversion is not None
577-
else -1
578-
),
579-
format_spec=(
580-
ast.Constant(value=part.format_spec)
581-
if part.format_spec != ""
582-
else None
583-
),
595+
value=parsed[interp_count],
596+
conversion=ord(part.conversion) if part.conversion else -1,
597+
format_spec=ast.Constant(value=part.format_spec)
598+
if part.format_spec
599+
else None,
584600
)
585601
values.append(interp)
602+
interp_count += 1
586603
return ast.TemplateStr(values=values)
587604

588605

606+
def _template_to_ast(template):
607+
"""Make a best-effort conversion of a `template` instance to an AST."""
608+
# gh-138558: Not all Template instances can be represented as t-string
609+
# literals. Return the most accurate AST we can. See issue for details.
610+
611+
# If any expr is empty or whitespace only, we cannot convert to a literal.
612+
if any(part.expression.strip() == "" for part in template.interpolations):
613+
return _template_to_ast_constructor(template)
614+
615+
try:
616+
# Wrap in parens to allow whitespace inside interpolation curly braces
617+
parsed = tuple(
618+
ast.parse(f"({part.expression})", mode="eval").body
619+
for part in template.interpolations
620+
)
621+
except SyntaxError:
622+
return _template_to_ast_constructor(template)
623+
624+
return _template_to_ast_literal(template, parsed)
625+
626+
589627
class _StringifierDict(dict):
590628
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
591629
super().__init__(namespace)

Lib/test/test_annotationlib.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import functools
88
import itertools
99
import pickle
10-
from string.templatelib import Template
10+
from string.templatelib import Template, Interpolation
1111
import typing
1212
import unittest
1313
from annotationlib import (
@@ -282,6 +282,7 @@ def f(
282282
a: t"a{b}c{d}e{f}g",
283283
b: t"{a:{1}}",
284284
c: t"{a | b * c}",
285+
gh138558: t"{ 0}",
285286
): pass
286287

287288
annos = get_annotations(f, format=Format.STRING)
@@ -293,6 +294,7 @@ def f(
293294
# interpolations in the format spec are eagerly evaluated so we can't recover the source
294295
"b": "t'{a:1}'",
295296
"c": "t'{a | b * c}'",
297+
"gh138558": "t'{ 0}'",
296298
})
297299

298300
def g(
@@ -1350,6 +1352,24 @@ def nested():
13501352
self.assertEqual(type_repr("1"), "'1'")
13511353
self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE))
13521354
self.assertEqual(type_repr(MyClass()), "my repr")
1355+
# gh138558 tests
1356+
self.assertEqual(type_repr(t'''{ 0
1357+
& 1
1358+
| 2
1359+
}'''), 't"""{ 0\n & 1\n | 2}"""')
1360+
self.assertEqual(
1361+
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
1362+
)
1363+
self.assertEqual(
1364+
type_repr(Template("hi", Interpolation(42))),
1365+
"Template('hi', Interpolation(42, '', None, ''))",
1366+
)
1367+
self.assertEqual(
1368+
type_repr(Template("hi", Interpolation(42, " "))),
1369+
"Template('hi', Interpolation(42, ' ', None, ''))",
1370+
)
1371+
# gh138558: perhaps in the future, we can improve this behavior:
1372+
self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'")
13531373

13541374

13551375
class TestAnnotationsToString(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix handling of unusual t-string annotations in annotationlib. Patch by Dave Peck.

0 commit comments

Comments
 (0)