diff --git a/src/griffe/__init__.py b/src/griffe/__init__.py index c06aad3a3..6bf664b6a 100644 --- a/src/griffe/__init__.py +++ b/src/griffe/__init__.py @@ -298,6 +298,7 @@ ExprFormatted, ExprGeneratorExp, ExprIfExp, + ExprInterpolation, ExprJoinedStr, ExprKeyword, ExprLambda, @@ -310,6 +311,7 @@ ExprSetComp, ExprSlice, ExprSubscript, + ExprTemplateStr, ExprTuple, ExprUnaryOp, ExprVarKeyword, @@ -453,6 +455,7 @@ "ExprFormatted", "ExprGeneratorExp", "ExprIfExp", + "ExprInterpolation", "ExprJoinedStr", "ExprKeyword", "ExprLambda", @@ -465,6 +468,7 @@ "ExprSetComp", "ExprSlice", "ExprSubscript", + "ExprTemplateStr", "ExprTuple", "ExprUnaryOp", "ExprVarKeyword", diff --git a/src/griffe/_internal/expressions.py b/src/griffe/_internal/expressions.py index 702bc56aa..6e6b9c02a 100644 --- a/src/griffe/_internal/expressions.py +++ b/src/griffe/_internal/expressions.py @@ -7,6 +7,7 @@ from __future__ import annotations import ast +import sys from dataclasses import dataclass from dataclasses import fields as getfields from enum import IntEnum, auto @@ -532,6 +533,20 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.orelse, flat=flat, outer_precedence=precedence, is_left=False) +@dataclass(eq=True, slots=True) +class ExprInterpolation(Expr): + """Template string interpolation like `{name}`.""" + + value: str | Expr + """Interpolated value.""" + + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: + yield "{" + # Prevent parentheses from being added, avoiding `{(1 + 1)}` + yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE) + yield "}" + + @dataclass(eq=True, slots=True) class ExprJoinedStr(Expr): """Joined strings like `f"a {b} c"`.""" @@ -915,6 +930,19 @@ def canonical_path(self) -> str: return self.left.canonical_path +@dataclass(eq=True, slots=True) +class ExprTemplateStr(Expr): + """Template strings like `t"a {name}"`.""" + + values: Sequence[str | Expr] + """Joined values.""" + + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: + yield "t'" + yield from _join(self.values, "", flat=flat) + yield "'" + + @dataclass(eq=True, slots=True) class ExprTuple(Expr): """Tuples like `(0, 1, 2)`.""" @@ -1213,6 +1241,12 @@ def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr ) +if sys.version_info >= (3, 14): + + def _build_interpolation(node: ast.Interpolation, parent: Module | Class, **kwargs: Any) -> Expr: + return ExprInterpolation(_build(node.value, parent, **kwargs)) + + def _build_joinedstr( node: ast.JoinedStr, parent: Module | Class, @@ -1311,6 +1345,16 @@ def _build_subscript( return ExprSubscript(left, slice_expr) +if sys.version_info >= (3, 14): + + def _build_templatestr( + node: ast.TemplateStr, + parent: Module | Class, + **kwargs: Any, + ) -> Expr: + return ExprTemplateStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values]) + + def _build_tuple( node: ast.Tuple, parent: Module | Class, @@ -1369,6 +1413,14 @@ def __call__(self, node: Any, parent: Module | Class, **kwargs: Any) -> Expr: .. ast.YieldFrom: _build_yield_from, } +if sys.version_info >= (3, 14): + _node_map.update( + { + ast.Interpolation: _build_interpolation, + ast.TemplateStr: _build_templatestr, + }, + ) + def _build(node: ast.AST, parent: Module | Class, /, **kwargs: Any) -> Expr: return _node_map[type(node)](node, parent, **kwargs) diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 27cfcc53c..3080401a3 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import sys from ast import PyCF_ONLY_AST import pytest @@ -51,6 +52,7 @@ "call(something=something)", # Strings. "f'a {round(key, 2)} {z}'", + *(["t'a {round(key, 2)} {z}'"] if sys.version_info >= (3, 14) else []), # Slices. "o[x]", "o[x, y]",