https://github.com/python/cpython/commit/ed0e63fd0abcbb68a384f8b5baccf616a5534570
commit: ed0e63fd0abcbb68a384f8b5baccf616a5534570
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-10-07T21:23:22-07:00
summary:

[3.14] gh-138558: Improve handling of Template annotations in annotationlib 
(GH-139072) (#139272)

gh-138558: Improve handling of Template annotations in annotationlib (GH-139072)
(cherry picked from commit 6ec058a1f7fcc016fa3b7432bcd0aa6e7d2b21ce)

Co-authored-by: Dave Peck <[email protected]>

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]

Reply via email to