https://github.com/python/cpython/commit/9e1aa354aed03cff100b3ff0d1d6d13ecbb370ab
commit: 9e1aa354aed03cff100b3ff0d1d6d13ecbb370ab
branch: 3.14
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-08T01:37:03Z
summary:

[3.14] gh-133551: Support t-strings in annotationlib (GH-133553) (#133628)

gh-133551: Support t-strings in annotationlib (GH-133553)

I don't know why you'd use t-strings in annotations, but now if you do,
the STRING format will do a great job of recovering the source code.
(cherry picked from commit 90f476e0f8dbb3a8603f67200c2422fb098c166c)

Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-05-06-22-54-37.gh-issue-133551.rfy1tJ.rst
M Lib/annotationlib.py
M Lib/test/.ruff.toml
M Lib/test/test_annotationlib.py

diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index c0b1d4395d14ed..32b8553458930c 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -305,6 +305,9 @@ def __repr__(self):
         return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
 
 
+_Template = type(t"")
+
+
 class _Stringifier:
     # Must match the slots on ForwardRef, so we can turn an instance of one 
into an
     # instance of the other in place.
@@ -341,6 +344,8 @@ def __convert_to_ast(self, other):
             if isinstance(other.__ast_node__, str):
                 return ast.Name(id=other.__ast_node__), other.__extra_names__
             return other.__ast_node__, other.__extra_names__
+        elif type(other) is _Template:
+            return _template_to_ast(other), None
         elif (
             # In STRING format we don't bother with the create_unique_name() 
dance;
             # it's better to emit the repr() of the object instead of an 
opaque name.
@@ -560,6 +565,32 @@ def unary_op(self):
     del _make_unary_op
 
 
+def _template_to_ast(template):
+    values = []
+    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
+                    ),
+                )
+                values.append(interp)
+    return ast.TemplateStr(values=values)
+
+
 class _StringifierDict(dict):
     def __init__(self, namespace, *, globals=None, owner=None, is_class=False, 
format):
         super().__init__(namespace)
@@ -784,6 +815,8 @@ def _stringify_single(anno):
     # We have to handle str specially to support PEP 563 stringified 
annotations.
     elif isinstance(anno, str):
         return anno
+    elif isinstance(anno, _Template):
+        return ast.unparse(_template_to_ast(anno))
     else:
         return repr(anno)
 
@@ -976,6 +1009,9 @@ def type_repr(value):
         if value.__module__ == "builtins":
             return value.__qualname__
         return f"{value.__module__}.{value.__qualname__}"
+    elif isinstance(value, _Template):
+        tree = _template_to_ast(value)
+        return ast.unparse(tree)
     if value is ...:
         return "..."
     return repr(value)
diff --git a/Lib/test/.ruff.toml b/Lib/test/.ruff.toml
index a1eac32a83aae3..7aa8a4785d6844 100644
--- a/Lib/test/.ruff.toml
+++ b/Lib/test/.ruff.toml
@@ -9,8 +9,9 @@ extend-exclude = [
     "encoded_modules/module_iso_8859_1.py",
     "encoded_modules/module_koi8_r.py",
     # SyntaxError because of t-strings
-    "test_tstring.py",
+    "test_annotationlib.py",
     "test_string/test_templatelib.py",
+    "test_tstring.py",
     # New grammar constructions may not yet be recognized by Ruff,
     # and tests re-use the same names as only the grammar is being checked.
     "test_grammar.py",
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index c3c245ddaf86d1..4af97c82de9d46 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -7,6 +7,7 @@
 import functools
 import itertools
 import pickle
+from string.templatelib import Interpolation, Template
 import typing
 import unittest
 from annotationlib import (
@@ -273,6 +274,43 @@ def f(
             },
         )
 
+    def test_template_str(self):
+        def f(
+            x: t"{a}",
+            y: list[t"{a}"],
+            z: t"{a:b} {c!r} {d!s:t}",
+            a: t"a{b}c{d}e{f}g",
+            b: t"{a:{1}}",
+            c: t"{a | b * c}",
+        ): pass
+
+        annos = get_annotations(f, format=Format.STRING)
+        self.assertEqual(annos, {
+            "x": "t'{a}'",
+            "y": "list[t'{a}']",
+            "z": "t'{a:b} {c!r} {d!s:t}'",
+            "a": "t'a{b}c{d}e{f}g'",
+            # interpolations in the format spec are eagerly evaluated so we 
can't recover the source
+            "b": "t'{a:1}'",
+            "c": "t'{a | b * c}'",
+        })
+
+        def g(
+            x: t"{a}",
+        ): ...
+
+        annos = get_annotations(g, format=Format.FORWARDREF)
+        templ = annos["x"]
+        # Template and Interpolation don't have __eq__ so we have to compare 
manually
+        self.assertIsInstance(templ, Template)
+        self.assertEqual(templ.strings, ("", ""))
+        self.assertEqual(len(templ.interpolations), 1)
+        interp = templ.interpolations[0]
+        self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g))
+        self.assertEqual(interp.expression, "a")
+        self.assertIsNone(interp.conversion)
+        self.assertEqual(interp.format_spec, "")
+
     def test_getitem(self):
         def f(x: undef1[str, undef2]):
             pass
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-06-22-54-37.gh-issue-133551.rfy1tJ.rst 
b/Misc/NEWS.d/next/Library/2025-05-06-22-54-37.gh-issue-133551.rfy1tJ.rst
new file mode 100644
index 00000000000000..7fedc0818dc469
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-06-22-54-37.gh-issue-133551.rfy1tJ.rst
@@ -0,0 +1,2 @@
+Support t-strings (:pep:`750`) in :mod:`annotationlib`. Patch by Jelle
+Zijlstra.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to