Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/reference/api/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@

::: griffe.ExprIfExp

::: griffe.ExprInterpolation

::: griffe.ExprJoinedStr

::: griffe.ExprKeyword
Expand Down Expand Up @@ -88,6 +90,8 @@

::: griffe.ExprSubscript

::: griffe.ExprTemplateStr

::: griffe.ExprTuple

::: griffe.ExprUnaryOp
Expand Down
4 changes: 4 additions & 0 deletions src/griffe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@
ExprFormatted,
ExprGeneratorExp,
ExprIfExp,
ExprInterpolation,
ExprJoinedStr,
ExprKeyword,
ExprLambda,
Expand All @@ -310,6 +311,7 @@
ExprSetComp,
ExprSlice,
ExprSubscript,
ExprTemplateStr,
ExprTuple,
ExprUnaryOp,
ExprVarKeyword,
Expand Down Expand Up @@ -453,6 +455,7 @@
"ExprFormatted",
"ExprGeneratorExp",
"ExprIfExp",
"ExprInterpolation",
"ExprJoinedStr",
"ExprKeyword",
"ExprLambda",
Expand All @@ -465,6 +468,7 @@
"ExprSetComp",
"ExprSlice",
"ExprSubscript",
"ExprTemplateStr",
"ExprTuple",
"ExprUnaryOp",
"ExprVarKeyword",
Expand Down
47 changes: 47 additions & 0 deletions src/griffe/_internal/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`."""
Expand Down Expand Up @@ -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 "'"
Comment on lines +940 to +943
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that using hardcoded single (or double) quotes will work in every case? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I referred to the implementation of ExprJoinedStr for this. As far as I can tell, this iterate method is only used when round-tripping in tests or dumping expressions by __str__ - it doesn't seem like it's very critical? Please correct me if I misunderstand.

I imagine this round-tripping could break when strings contain certain types of quotes - for example, if input value is t"a'b", converting this to t'a'b' would invalidate syntax. However, if we have the same problem with ExprJoinedStr and ExprTemplateStr, I propose creating a new project issue to improve quote handling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's critical: it must yield correct code. This code is then parsed to be highlighted, formatted etc. by Black/Ruff/Pygments/else in downstream tools like mkdocstrings-python.

However, if we have the same problem with ExprJoinedStr and ExprTemplateStr, I propose creating a new project issue to improve quote handling.

You make a good point. Lets handle that in a new issue then 🙂



@dataclass(eq=True, slots=True)
class ExprTuple(Expr):
"""Tuples like `(0, 1, 2)`."""
Expand Down Expand Up @@ -1369,6 +1397,25 @@ def __call__(self, node: Any, parent: Module | Class, **kwargs: Any) -> Expr: ..
ast.YieldFrom: _build_yield_from,
}

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_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])

_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)
Expand Down
22 changes: 19 additions & 3 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
from __future__ import annotations

import logging
import sys
from ast import PyCF_ONLY_AST

import pytest

from griffe import Expr, ExprName, module_vtree, relative_to_absolute, temporary_visited_module
from griffe import (
Expr,
ExprName,
module_vtree,
relative_to_absolute,
temporary_visited_module,
)

syntax_examples = [
# Operations.
Expand Down Expand Up @@ -51,6 +58,8 @@
"call(something=something)",
# Strings.
"f'a {round(key, 2)} {z}'",
# YORE: EOL 3.13: Replace line with `"t'a {round(key, 2)} {z}'",`.
*(["t'a {round(key, 2)} {z}'"] if sys.version_info >= (3, 14) else []),
# Slices.
"o[x]",
"o[x, y]",
Expand Down Expand Up @@ -107,7 +116,12 @@
("from ...c.d import *", "a.c.d.i", False, "a.c.d.*"),
],
)
def test_relative_to_absolute_imports(code: str, path: str, is_package: bool, expected: str) -> None:
def test_relative_to_absolute_imports(
code: str,
path: str,
is_package: bool,
expected: str,
) -> None:
"""Check if relative imports are correctly converted to absolute ones.

Parameters:
Expand Down Expand Up @@ -158,7 +172,9 @@ def test_building_annotations_from_nodes(expression: str) -> None:
expression: An expression (parametrized).
"""
class_defs = "\n\n".join(f"class {letter}: ..." for letter in "ABCD")
with temporary_visited_module(f"{class_defs}\n\nx: {expression}\ny: {expression} = 0") as module:
with temporary_visited_module(
f"{class_defs}\n\nx: {expression}\ny: {expression} = 0",
) as module:
assert "x" in module.members
assert "y" in module.members
assert str(module["x"].annotation) == expression
Expand Down
Loading