mirror of
https://github.com/python/cpython.git
synced 2025-09-18 14:40:43 +00:00
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:
parent
2cc6de77bd
commit
90f476e0f8
4 changed files with 78 additions and 1 deletions
|
@ -305,6 +305,9 @@ class ForwardRef:
|
||||||
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
|
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
|
||||||
|
|
||||||
|
|
||||||
|
_Template = type(t"")
|
||||||
|
|
||||||
|
|
||||||
class _Stringifier:
|
class _Stringifier:
|
||||||
# Must match the slots on ForwardRef, so we can turn an instance of one into an
|
# Must match the slots on ForwardRef, so we can turn an instance of one into an
|
||||||
# instance of the other in place.
|
# instance of the other in place.
|
||||||
|
@ -341,6 +344,8 @@ class _Stringifier:
|
||||||
if isinstance(other.__ast_node__, str):
|
if isinstance(other.__ast_node__, str):
|
||||||
return ast.Name(id=other.__ast_node__), other.__extra_names__
|
return ast.Name(id=other.__ast_node__), other.__extra_names__
|
||||||
return 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 (
|
elif (
|
||||||
# In STRING format we don't bother with the create_unique_name() dance;
|
# 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.
|
# 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
|
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):
|
class _StringifierDict(dict):
|
||||||
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
|
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
|
||||||
super().__init__(namespace)
|
super().__init__(namespace)
|
||||||
|
@ -784,6 +815,8 @@ def _stringify_single(anno):
|
||||||
# We have to handle str specially to support PEP 563 stringified annotations.
|
# We have to handle str specially to support PEP 563 stringified annotations.
|
||||||
elif isinstance(anno, str):
|
elif isinstance(anno, str):
|
||||||
return anno
|
return anno
|
||||||
|
elif isinstance(anno, _Template):
|
||||||
|
return ast.unparse(_template_to_ast(anno))
|
||||||
else:
|
else:
|
||||||
return repr(anno)
|
return repr(anno)
|
||||||
|
|
||||||
|
@ -976,6 +1009,9 @@ def type_repr(value):
|
||||||
if value.__module__ == "builtins":
|
if value.__module__ == "builtins":
|
||||||
return value.__qualname__
|
return value.__qualname__
|
||||||
return f"{value.__module__}.{value.__qualname__}"
|
return f"{value.__module__}.{value.__qualname__}"
|
||||||
|
elif isinstance(value, _Template):
|
||||||
|
tree = _template_to_ast(value)
|
||||||
|
return ast.unparse(tree)
|
||||||
if value is ...:
|
if value is ...:
|
||||||
return "..."
|
return "..."
|
||||||
return repr(value)
|
return repr(value)
|
||||||
|
|
|
@ -9,8 +9,9 @@ extend-exclude = [
|
||||||
"encoded_modules/module_iso_8859_1.py",
|
"encoded_modules/module_iso_8859_1.py",
|
||||||
"encoded_modules/module_koi8_r.py",
|
"encoded_modules/module_koi8_r.py",
|
||||||
# SyntaxError because of t-strings
|
# SyntaxError because of t-strings
|
||||||
"test_tstring.py",
|
"test_annotationlib.py",
|
||||||
"test_string/test_templatelib.py",
|
"test_string/test_templatelib.py",
|
||||||
|
"test_tstring.py",
|
||||||
# New grammar constructions may not yet be recognized by Ruff,
|
# New grammar constructions may not yet be recognized by Ruff,
|
||||||
# and tests re-use the same names as only the grammar is being checked.
|
# and tests re-use the same names as only the grammar is being checked.
|
||||||
"test_grammar.py",
|
"test_grammar.py",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import collections
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import pickle
|
import pickle
|
||||||
|
from string.templatelib import Interpolation, Template
|
||||||
import typing
|
import typing
|
||||||
import unittest
|
import unittest
|
||||||
from annotationlib import (
|
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 test_getitem(self):
|
||||||
def f(x: undef1[str, undef2]):
|
def f(x: undef1[str, undef2]):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Support t-strings (:pep:`750`) in :mod:`annotationlib`. Patch by Jelle
|
||||||
|
Zijlstra.
|
Loading…
Add table
Add a link
Reference in a new issue