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.
This commit is contained in:
Jelle Zijlstra 2025-05-07 18:10:35 -07:00 committed by GitHub
parent 2cc6de77bd
commit 90f476e0f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 1 deletions

View file

@ -305,6 +305,9 @@ class ForwardRef:
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 @@ class _Stringifier:
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 @@ class _Stringifier:
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)

View file

@ -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",

View file

@ -7,6 +7,7 @@ import collections
import functools
import itertools
import pickle
from string.templatelib import Interpolation, Template
import typing
import unittest
from annotationlib import (
@ -273,6 +274,43 @@ class TestStringFormat(unittest.TestCase):
},
)
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

View file

@ -0,0 +1,2 @@
Support t-strings (:pep:`750`) in :mod:`annotationlib`. Patch by Jelle
Zijlstra.