Skip to content

Commit f866029

Browse files
committed
Improve annotationlib._template_to_ast() behavior
1 parent 530ddd3 commit f866029

1 file changed

Lines changed: 62 additions & 13 deletions

File tree

Lib/annotationlib.py

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

562562

563-
def _template_to_ast(template):
563+
def _conversion_ast_value(conversion):
564+
"""Convert a `conversion` character to an AST-friendly Constant."""
565+
return -1 if conversion is None else ord(conversion)
566+
567+
568+
def _format(format_spec):
569+
"""Convert a `format_spec` string to an AST-friendly Constant."""
570+
value = format_spec or None # empty string -> None
571+
return ast.Constant(value=value)
572+
573+
574+
def _template_to_ast_constructor(template):
575+
"""Convert a `template` instance to a non-literal AST."""
576+
args = []
577+
for part in template:
578+
match part:
579+
case str():
580+
args.append(ast.Constant(value=part))
581+
case _:
582+
interp = ast.Call(
583+
func=ast.Name(id="Interpolation"),
584+
args=[
585+
ast.Constant(value=part.value),
586+
ast.Constant(value=part.expression),
587+
ast.Constant(value=part.conversion),
588+
ast.Constant(value=part.format_spec),
589+
]
590+
)
591+
args.append(interp)
592+
return ast.Call(
593+
func=ast.Name(id="Template"),
594+
args=args,
595+
keywords=[],
596+
)
597+
598+
599+
def _template_to_ast_literal(template, parsed):
600+
"""Convert a `template` instance to a t-string literal AST."""
564601
values = []
602+
interp_count = 0
565603
for part in template:
566604
match part:
567605
case str():
568606
values.append(ast.Constant(value=part))
569-
# Interpolation, but we don't want to import the string module
570607
case _:
571608
interp = ast.Interpolation(
572609
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-
),
610+
value=parsed[interp_count],
611+
conversion=ord(part.conversion) if part.conversion else -1,
612+
format_spec=part.format_spec or None, # "" -> None
584613
)
585614
values.append(interp)
615+
interp_count += 1
586616
return ast.TemplateStr(values=values)
587617

588618

619+
def _template_to_ast(template):
620+
"""Make a best-effort conversion of a `template` instance to an AST."""
621+
# gh-138558: Not all Template instances can be represented as t-string
622+
# literals. Return the most accurate AST we can. See issue for details.
623+
if any(part.expression == "" for part in template.interpolations):
624+
return _template_to_ast_constructor(template)
625+
626+
try:
627+
# Wrap in parens to allow whitespace inside interpolation curly braces
628+
parsed = tuple(
629+
ast.parse(f"({part.expression})", mode="eval").body
630+
for part in template.interpolations
631+
)
632+
except SyntaxError:
633+
return _template_to_ast_constructor(template)
634+
635+
return _template_to_ast_literal(template, parsed)
636+
637+
589638
class _StringifierDict(dict):
590639
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
591640
super().__init__(namespace)

0 commit comments

Comments
 (0)