https://github.com/python/cpython/commit/6ec058a1f7fcc016fa3b7432bcd0aa6e7d2b21ce
commit: 6ec058a1f7fcc016fa3b7432bcd0aa6e7d2b21ce
branch: main
author: Dave Peck <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-09-23T11:25:51-07:00
summary:
gh-138558: Improve handling of Template annotations in annotationlib (#139072)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-17-17-21.gh-issue-138558.0VbzCH.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index bee019cd51591e..43e1d51bc4b807 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -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)
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 88e0d611647f28..a8a8bcec76a429 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -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 (
@@ -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)
@@ -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(
@@ -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):
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-17-17-21.gh-issue-138558.0VbzCH.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-17-17-21.gh-issue-138558.0VbzCH.rst
new file mode 100644
index 00000000000000..23c995d2452f7b
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-17-17-21.gh-issue-138558.0VbzCH.rst
@@ -0,0 +1 @@
+Fix handling of unusual t-string annotations in annotationlib. Patch by Dave
Peck.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]