https://github.com/python/cpython/commit/90f476e0f8dbb3a8603f67200c2422fb098c166c
commit: 90f476e0f8dbb3a8603f67200c2422fb098c166c
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-05-07T18:10:35-07:00
summary:
gh-133551: Support t-strings in annotationlib (#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.
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 -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]