Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 51 additions & 13 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,32 +560,70 @@ def unary_op(self):
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)
Expand Down
22 changes: 21 additions & 1 deletion Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import functools
import itertools
import pickle
from string.templatelib import Template
from string.templatelib import Template, Interpolation
import typing
import unittest
from annotationlib import (
Expand Down Expand Up @@ -282,6 +282,7 @@ def f(
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)
Expand All @@ -293,6 +294,7 @@ def f(
# 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(
Expand Down Expand Up @@ -1350,6 +1352,24 @@ def nested():
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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix handling of unusual t-string annotations in annotationlib. Patch by Dave Peck.
Loading