mirror of
https://github.com/python/cpython.git
synced 2025-11-24 12:20:42 +00:00
[3.14] gh-138558: Improve handling of Template annotations in annotationlib (GH-139072) (#139272)
gh-138558: Improve handling of Template annotations in annotationlib (GH-139072)
(cherry picked from commit 6ec058a1f7)
Co-authored-by: Dave Peck <davepeck@gmail.com>
This commit is contained in:
parent
9e0b7d0866
commit
ed0e63fd0a
3 changed files with 73 additions and 14 deletions
|
|
@ -560,32 +560,70 @@ class _Stringifier:
|
|||
del _make_unary_op
|
||||
|
||||
|
||||
def _template_to_ast(template):
|
||||
def _template_to_ast_constructor(template):
|
||||
"""Convert a `template` instance to a non-literal AST."""
|
||||
args = []
|
||||
for part in template:
|
||||
match part:
|
||||
case str():
|
||||
args.append(ast.Constant(value=part))
|
||||
case _:
|
||||
interp = ast.Call(
|
||||
func=ast.Name(id="Interpolation"),
|
||||
args=[
|
||||
ast.Constant(value=part.value),
|
||||
ast.Constant(value=part.expression),
|
||||
ast.Constant(value=part.conversion),
|
||||
ast.Constant(value=part.format_spec),
|
||||
]
|
||||
)
|
||||
args.append(interp)
|
||||
return ast.Call(func=ast.Name(id="Template"), args=args, keywords=[])
|
||||
|
||||
|
||||
def _template_to_ast_literal(template, parsed):
|
||||
"""Convert a `template` instance to a t-string literal AST."""
|
||||
values = []
|
||||
interp_count = 0
|
||||
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
|
||||
),
|
||||
value=parsed[interp_count],
|
||||
conversion=ord(part.conversion) if part.conversion else -1,
|
||||
format_spec=ast.Constant(value=part.format_spec)
|
||||
if part.format_spec
|
||||
else None,
|
||||
)
|
||||
values.append(interp)
|
||||
interp_count += 1
|
||||
return ast.TemplateStr(values=values)
|
||||
|
||||
|
||||
def _template_to_ast(template):
|
||||
"""Make a best-effort conversion of a `template` instance to an AST."""
|
||||
# gh-138558: Not all Template instances can be represented as t-string
|
||||
# literals. Return the most accurate AST we can. See issue for details.
|
||||
|
||||
# If any expr is empty or whitespace only, we cannot convert to a literal.
|
||||
if any(part.expression.strip() == "" for part in template.interpolations):
|
||||
return _template_to_ast_constructor(template)
|
||||
|
||||
try:
|
||||
# Wrap in parens to allow whitespace inside interpolation curly braces
|
||||
parsed = tuple(
|
||||
ast.parse(f"({part.expression})", mode="eval").body
|
||||
for part in template.interpolations
|
||||
)
|
||||
except SyntaxError:
|
||||
return _template_to_ast_constructor(template)
|
||||
|
||||
return _template_to_ast_literal(template, parsed)
|
||||
|
||||
|
||||
class _StringifierDict(dict):
|
||||
def __init__(self, namespace, *, globals=None, owner=None, is_class=False, format):
|
||||
super().__init__(namespace)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import collections
|
|||
import functools
|
||||
import itertools
|
||||
import pickle
|
||||
from string.templatelib import Template
|
||||
from string.templatelib import Template, Interpolation
|
||||
import typing
|
||||
import unittest
|
||||
from annotationlib import (
|
||||
|
|
@ -282,6 +282,7 @@ class TestStringFormat(unittest.TestCase):
|
|||
a: t"a{b}c{d}e{f}g",
|
||||
b: t"{a:{1}}",
|
||||
c: t"{a | b * c}",
|
||||
gh138558: t"{ 0}",
|
||||
): pass
|
||||
|
||||
annos = get_annotations(f, format=Format.STRING)
|
||||
|
|
@ -293,6 +294,7 @@ class TestStringFormat(unittest.TestCase):
|
|||
# interpolations in the format spec are eagerly evaluated so we can't recover the source
|
||||
"b": "t'{a:1}'",
|
||||
"c": "t'{a | b * c}'",
|
||||
"gh138558": "t'{ 0}'",
|
||||
})
|
||||
|
||||
def g(
|
||||
|
|
@ -1350,6 +1352,24 @@ class TestTypeRepr(unittest.TestCase):
|
|||
self.assertEqual(type_repr("1"), "'1'")
|
||||
self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE))
|
||||
self.assertEqual(type_repr(MyClass()), "my repr")
|
||||
# gh138558 tests
|
||||
self.assertEqual(type_repr(t'''{ 0
|
||||
& 1
|
||||
| 2
|
||||
}'''), 't"""{ 0\n & 1\n | 2}"""')
|
||||
self.assertEqual(
|
||||
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
|
||||
)
|
||||
self.assertEqual(
|
||||
type_repr(Template("hi", Interpolation(42))),
|
||||
"Template('hi', Interpolation(42, '', None, ''))",
|
||||
)
|
||||
self.assertEqual(
|
||||
type_repr(Template("hi", Interpolation(42, " "))),
|
||||
"Template('hi', Interpolation(42, ' ', None, ''))",
|
||||
)
|
||||
# gh138558: perhaps in the future, we can improve this behavior:
|
||||
self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'")
|
||||
|
||||
|
||||
class TestAnnotationsToString(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Fix handling of unusual t-string annotations in annotationlib. Patch by Dave Peck.
|
||||
Loading…
Add table
Add a link
Reference in a new issue