Skip to content

Commit 90f476e

Browse files
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.
1 parent 2cc6de7 commit 90f476e

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

Lib/annotationlib.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ def __repr__(self):
305305
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
306306

307307

308+
_Template = type(t"")
309+
310+
308311
class _Stringifier:
309312
# Must match the slots on ForwardRef, so we can turn an instance of one into an
310313
# instance of the other in place.
@@ -341,6 +344,8 @@ def __convert_to_ast(self, other):
341344
if isinstance(other.__ast_node__, str):
342345
return ast.Name(id=other.__ast_node__), other.__extra_names__
343346
return other.__ast_node__, other.__extra_names__
347+
elif type(other) is _Template:
348+
return _template_to_ast(other), None
344349
elif (
345350
# In STRING format we don't bother with the create_unique_name() dance;
346351
# it's better to emit the repr() of the object instead of an opaque name.
@@ -560,6 +565,32 @@ def unary_op(self):
560565
del _make_unary_op
561566

562567

568+
def _template_to_ast(template):
569+
values = []
570+
for part in template:
571+
match part:
572+
case str():
573+
values.append(ast.Constant(value=part))
574+
# Interpolation, but we don't want to import the string module
575+
case _:
576+
interp = ast.Interpolation(
577+
str=part.expression,
578+
value=ast.parse(part.expression),
579+
conversion=(
580+
ord(part.conversion)
581+
if part.conversion is not None
582+
else -1
583+
),
584+
format_spec=(
585+
ast.Constant(value=part.format_spec)
586+
if part.format_spec != ""
587+
else None
588+
),
589+
)
590+
values.append(interp)
591+
return ast.TemplateStr(values=values)
592+
593+
563594
class _StringifierDict(dict):
564595
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
565596
super().__init__(namespace)
@@ -784,6 +815,8 @@ def _stringify_single(anno):
784815
# We have to handle str specially to support PEP 563 stringified annotations.
785816
elif isinstance(anno, str):
786817
return anno
818+
elif isinstance(anno, _Template):
819+
return ast.unparse(_template_to_ast(anno))
787820
else:
788821
return repr(anno)
789822

@@ -976,6 +1009,9 @@ def type_repr(value):
9761009
if value.__module__ == "builtins":
9771010
return value.__qualname__
9781011
return f"{value.__module__}.{value.__qualname__}"
1012+
elif isinstance(value, _Template):
1013+
tree = _template_to_ast(value)
1014+
return ast.unparse(tree)
9791015
if value is ...:
9801016
return "..."
9811017
return repr(value)

Lib/test/.ruff.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ extend-exclude = [
99
"encoded_modules/module_iso_8859_1.py",
1010
"encoded_modules/module_koi8_r.py",
1111
# SyntaxError because of t-strings
12-
"test_tstring.py",
12+
"test_annotationlib.py",
1313
"test_string/test_templatelib.py",
14+
"test_tstring.py",
1415
# New grammar constructions may not yet be recognized by Ruff,
1516
# and tests re-use the same names as only the grammar is being checked.
1617
"test_grammar.py",

Lib/test/test_annotationlib.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import functools
88
import itertools
99
import pickle
10+
from string.templatelib import Interpolation, Template
1011
import typing
1112
import unittest
1213
from annotationlib import (
@@ -273,6 +274,43 @@ def f(
273274
},
274275
)
275276

277+
def test_template_str(self):
278+
def f(
279+
x: t"{a}",
280+
y: list[t"{a}"],
281+
z: t"{a:b} {c!r} {d!s:t}",
282+
a: t"a{b}c{d}e{f}g",
283+
b: t"{a:{1}}",
284+
c: t"{a | b * c}",
285+
): pass
286+
287+
annos = get_annotations(f, format=Format.STRING)
288+
self.assertEqual(annos, {
289+
"x": "t'{a}'",
290+
"y": "list[t'{a}']",
291+
"z": "t'{a:b} {c!r} {d!s:t}'",
292+
"a": "t'a{b}c{d}e{f}g'",
293+
# interpolations in the format spec are eagerly evaluated so we can't recover the source
294+
"b": "t'{a:1}'",
295+
"c": "t'{a | b * c}'",
296+
})
297+
298+
def g(
299+
x: t"{a}",
300+
): ...
301+
302+
annos = get_annotations(g, format=Format.FORWARDREF)
303+
templ = annos["x"]
304+
# Template and Interpolation don't have __eq__ so we have to compare manually
305+
self.assertIsInstance(templ, Template)
306+
self.assertEqual(templ.strings, ("", ""))
307+
self.assertEqual(len(templ.interpolations), 1)
308+
interp = templ.interpolations[0]
309+
self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g))
310+
self.assertEqual(interp.expression, "a")
311+
self.assertIsNone(interp.conversion)
312+
self.assertEqual(interp.format_spec, "")
313+
276314
def test_getitem(self):
277315
def f(x: undef1[str, undef2]):
278316
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Support t-strings (:pep:`750`) in :mod:`annotationlib`. Patch by Jelle
2+
Zijlstra.

0 commit comments

Comments
 (0)