Skip to content

Commit b3c72a0

Browse files
authored
Add basic support for TemplateStr (#2793)
1 parent 2a081cd commit b3c72a0

File tree

7 files changed

+228
-0
lines changed

7 files changed

+228
-0
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ Release date: TBA
5252

5353
Closes #2672
5454

55+
* Add basic support for ``ast.TemplateStr`` and ``ast.Interpolation``added in Python 3.14.
56+
57+
refs #2789
58+
5559

5660
What's New in astroid 3.3.11?
5761
=============================

astroid/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
IfExp,
123123
Import,
124124
ImportFrom,
125+
Interpolation,
125126
JoinedStr,
126127
Keyword,
127128
Lambda,
@@ -151,6 +152,7 @@
151152
Slice,
152153
Starred,
153154
Subscript,
155+
TemplateStr,
154156
Try,
155157
TryStar,
156158
Tuple,

astroid/nodes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
IfExp,
5151
Import,
5252
ImportFrom,
53+
Interpolation,
5354
JoinedStr,
5455
Keyword,
5556
List,
@@ -76,6 +77,7 @@
7677
Slice,
7778
Starred,
7879
Subscript,
80+
TemplateStr,
7981
Try,
8082
TryStar,
8183
Tuple,
@@ -247,6 +249,7 @@
247249
"IfExp",
248250
"Import",
249251
"ImportFrom",
252+
"Interpolation",
250253
"JoinedStr",
251254
"Keyword",
252255
"Lambda",
@@ -278,6 +281,7 @@
278281
"Slice",
279282
"Starred",
280283
"Subscript",
284+
"TemplateStr",
281285
"Try",
282286
"TryStar",
283287
"Tuple",

astroid/nodes/as_string.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from astroid import objects
1717
from astroid.nodes import Const
1818
from astroid.nodes.node_classes import (
19+
Interpolation,
1920
Match,
2021
MatchAs,
2122
MatchCase,
@@ -26,6 +27,7 @@
2627
MatchSingleton,
2728
MatchStar,
2829
MatchValue,
30+
TemplateStr,
2931
Unknown,
3032
)
3133
from astroid.nodes.node_ng import NodeNG
@@ -672,6 +674,32 @@ def visit_matchor(self, node: MatchOr) -> str:
672674
raise AssertionError(f"{node} does not have pattern nodes")
673675
return " | ".join(p.accept(self) for p in node.patterns)
674676

677+
def visit_templatestr(self, node: TemplateStr) -> str:
678+
"""Return an astroid.TemplateStr node as string."""
679+
string = ""
680+
for value in node.values:
681+
match value:
682+
case nodes.Interpolation():
683+
string += "{" + value.accept(self) + "}"
684+
case _:
685+
string += value.accept(self)[1:-1]
686+
for quote in ("'", '"', '"""', "'''"):
687+
if quote not in string:
688+
break
689+
return "t" + quote + string + quote
690+
691+
def visit_interpolation(self, node: Interpolation) -> str:
692+
"""Return an astroid.Interpolation node as string."""
693+
result = f"{node.str}"
694+
if node.conversion and node.conversion >= 0:
695+
# e.g. if node.conversion == 114: result += "!r"
696+
result += "!" + chr(node.conversion)
697+
if node.format_spec:
698+
# The format spec is itself a JoinedString, i.e. an f-string
699+
# We strip the f and quotes of the ends
700+
result += ":" + node.format_spec.accept(self)[2:-1]
701+
return result
702+
675703
# These aren't for real AST nodes, but for inference objects.
676704

677705
def visit_frozenset(self, node: objects.FrozenSet) -> str:

astroid/nodes/node_classes.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5484,6 +5484,114 @@ def postinit(self, *, patterns: list[Pattern]) -> None:
54845484
self.patterns = patterns
54855485

54865486

5487+
class TemplateStr(NodeNG):
5488+
"""Class representing an :class:`ast.TemplateStr` node.
5489+
5490+
>>> import astroid
5491+
>>> node = astroid.extract_node('t"{name} finished {place!s}"')
5492+
>>> node
5493+
<TemplateStr l.1 at 0x103b7aa50>
5494+
"""
5495+
5496+
_astroid_fields = ("values",)
5497+
5498+
def __init__(
5499+
self,
5500+
lineno: int | None = None,
5501+
col_offset: int | None = None,
5502+
parent: NodeNG | None = None,
5503+
*,
5504+
end_lineno: int | None = None,
5505+
end_col_offset: int | None = None,
5506+
) -> None:
5507+
self.values: list[NodeNG]
5508+
super().__init__(
5509+
lineno=lineno,
5510+
col_offset=col_offset,
5511+
end_lineno=end_lineno,
5512+
end_col_offset=end_col_offset,
5513+
parent=parent,
5514+
)
5515+
5516+
def postinit(self, *, values: list[NodeNG]) -> None:
5517+
self.values = values
5518+
5519+
def get_children(self) -> Iterator[NodeNG]:
5520+
yield from self.values
5521+
5522+
5523+
class Interpolation(NodeNG):
5524+
"""Class representing an :class:`ast.Interpolation` node.
5525+
5526+
>>> import astroid
5527+
>>> node = astroid.extract_node('t"{name} finished {place!s}"')
5528+
>>> node
5529+
<TemplateStr l.1 at 0x103b7aa50>
5530+
>>> node.values[0]
5531+
<Interpolation l.1 at 0x103b7acf0>
5532+
>>> node.values[2]
5533+
<Interpolation l.1 at 0x10411e5d0>
5534+
"""
5535+
5536+
_astroid_fields = ("value", "format_spec")
5537+
_other_fields = ("str", "conversion")
5538+
5539+
def __init__(
5540+
self,
5541+
lineno: int | None = None,
5542+
col_offset: int | None = None,
5543+
parent: NodeNG | None = None,
5544+
*,
5545+
end_lineno: int | None = None,
5546+
end_col_offset: int | None = None,
5547+
) -> None:
5548+
self.value: NodeNG
5549+
"""Any expression node."""
5550+
5551+
self.str: str
5552+
"""Text of the interpolation expression."""
5553+
5554+
self.conversion: int
5555+
"""The type of formatting to be applied to the value.
5556+
5557+
.. seealso::
5558+
:class:`ast.Interpolation`
5559+
"""
5560+
5561+
self.format_spec: JoinedStr | None = None
5562+
"""The formatting to be applied to the value.
5563+
5564+
.. seealso::
5565+
:class:`ast.Interpolation`
5566+
"""
5567+
5568+
super().__init__(
5569+
lineno=lineno,
5570+
col_offset=col_offset,
5571+
end_lineno=end_lineno,
5572+
end_col_offset=end_col_offset,
5573+
parent=parent,
5574+
)
5575+
5576+
def postinit(
5577+
self,
5578+
*,
5579+
value: NodeNG,
5580+
str: str, # pylint: disable=redefined-builtin
5581+
conversion: int = -1,
5582+
format_spec: JoinedStr | None = None,
5583+
) -> None:
5584+
self.value = value
5585+
self.str = str
5586+
self.conversion = conversion
5587+
self.format_spec = format_spec
5588+
5589+
def get_children(self) -> Iterator[NodeNG]:
5590+
yield self.value
5591+
if self.format_spec:
5592+
yield self.format_spec
5593+
5594+
54875595
# constants ##############################################################
54885596

54895597
# The _proxied attribute of all container types (List, Tuple, etc.)

astroid/rebuilder.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,18 @@ def visit(self, node: ast.MatchOr, parent: nodes.NodeNG) -> nodes.MatchOr: ...
460460
@overload
461461
def visit(self, node: ast.pattern, parent: nodes.NodeNG) -> nodes.Pattern: ...
462462

463+
if sys.version_info >= (3, 14):
464+
465+
@overload
466+
def visit(
467+
self, node: ast.TemplateStr, parent: nodes.NodeNG
468+
) -> nodes.TemplateStr: ...
469+
470+
@overload
471+
def visit(
472+
self, node: ast.Interpolation, parent: nodes.NodeNG
473+
) -> nodes.Interpolation: ...
474+
463475
@overload
464476
def visit(self, node: ast.AST, parent: nodes.NodeNG) -> nodes.NodeNG: ...
465477

@@ -1929,3 +1941,38 @@ def visit_matchor(self, node: ast.MatchOr, parent: nodes.NodeNG) -> nodes.MatchO
19291941
patterns=[self.visit(pattern, newnode) for pattern in node.patterns]
19301942
)
19311943
return newnode
1944+
1945+
if sys.version_info >= (3, 14):
1946+
1947+
def visit_templatestr(
1948+
self, node: ast.TemplateStr, parent: nodes.NodeNG
1949+
) -> nodes.TemplateStr:
1950+
newnode = nodes.TemplateStr(
1951+
lineno=node.lineno,
1952+
col_offset=node.col_offset,
1953+
end_lineno=node.end_lineno,
1954+
end_col_offset=node.end_col_offset,
1955+
parent=parent,
1956+
)
1957+
newnode.postinit(
1958+
values=[self.visit(value, newnode) for value in node.values]
1959+
)
1960+
return newnode
1961+
1962+
def visit_interpolation(
1963+
self, node: ast.Interpolation, parent: nodes.NodeNG
1964+
) -> nodes.Interpolation:
1965+
newnode = nodes.Interpolation(
1966+
lineno=node.lineno,
1967+
col_offset=node.col_offset,
1968+
end_lineno=node.end_lineno,
1969+
end_col_offset=node.end_col_offset,
1970+
parent=parent,
1971+
)
1972+
newnode.postinit(
1973+
value=self.visit(node.value, parent),
1974+
str=node.str,
1975+
conversion=node.conversion,
1976+
format_spec=self.visit(node.format_spec, parent),
1977+
)
1978+
return newnode

tests/test_nodes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,6 +2173,41 @@ def return_from_match(x):
21732173
assert [inf.value for inf in inferred] == [10, -1]
21742174

21752175

2176+
@pytest.mark.skipif(not PY314_PLUS, reason="TemplateStr was added in PY314")
2177+
class TestTemplateString:
2178+
@staticmethod
2179+
def test_template_string_simple() -> None:
2180+
code = textwrap.dedent(
2181+
"""
2182+
name = "Foo"
2183+
place = 3
2184+
t"{name} finished {place!r:ordinal}" #@
2185+
"""
2186+
).strip()
2187+
node = builder.extract_node(code)
2188+
assert node.as_string() == "t'{name} finished {place!r:ordinal}'"
2189+
assert isinstance(node, nodes.TemplateStr)
2190+
assert len(node.values) == 3
2191+
value = node.values[0]
2192+
assert isinstance(value, nodes.Interpolation)
2193+
assert isinstance(value.value, nodes.Name)
2194+
assert value.value.name == "name"
2195+
assert value.str == "name"
2196+
assert value.conversion == -1
2197+
assert value.format_spec is None
2198+
value = node.values[1]
2199+
assert isinstance(value, nodes.Const)
2200+
assert value.pytype() == "builtins.str"
2201+
assert value.value == " finished "
2202+
value = node.values[2]
2203+
assert isinstance(value, nodes.Interpolation)
2204+
assert isinstance(value.value, nodes.Name)
2205+
assert value.value.name == "place"
2206+
assert value.str == "place"
2207+
assert value.conversion == ord("r")
2208+
assert isinstance(value.format_spec, nodes.JoinedStr)
2209+
2210+
21762211
@pytest.mark.parametrize(
21772212
"node",
21782213
[

0 commit comments

Comments
 (0)