Skip to content

Commit a8c5585

Browse files
abc8747pawamoy
andauthored
fix: Correctly parenthesize expressions
Previously, stringifying expressions would concatenate the string representation of their parts without considering the order of operations, leading to incorrect removal of semantically necessary parentheses. This commit changes the following things: - the binding strength of operators (1 for walrus, 18 for atoms) were added - parentheses are added when: - inner operation has a lower precedence than an outer one (e.g. `(a + b) * c`) - subexpression on the right of left associative operators have the same precedence (e.g. `(a - b) - c`) - subexpression on the left of right associative operators have the same precedence (e.g. `(a ** b) ** c`) - special cases like power operator binding less when an arithmetic or bitwise operator on its right were implemented PR-389: #389 Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr>
1 parent 0a05186 commit a8c5585

File tree

2 files changed

+225
-32
lines changed

2 files changed

+225
-32
lines changed

src/_griffe/expressions.py

Lines changed: 195 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import sys
1111
from dataclasses import dataclass
1212
from dataclasses import fields as getfields
13+
from enum import IntEnum, auto
1314
from functools import partial
14-
from itertools import zip_longest
1515
from typing import TYPE_CHECKING, Any, Callable
1616

1717
from _griffe.agents.nodes.parameters import get_parameters
@@ -26,14 +26,82 @@
2626
from _griffe.models import Class, Function, Module
2727

2828

29-
def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]:
30-
if isinstance(element, str):
31-
yield element
29+
class _OperatorPrecedence(IntEnum):
30+
# Adapted from:
31+
#
32+
# - https://docs.python.org/3/reference/expressions.html#operator-precedence
33+
# - https://github.com/python/cpython/blob/main/Lib/_ast_unparse.py
34+
# - https://github.com/astral-sh/ruff/blob/6abafcb56575454f2caeaa174efcb9fd0a8362b1/crates/ruff_python_ast/src/operator_precedence.rs
35+
36+
# The enum members are declared in ascending order of precedence.
37+
38+
# A virtual precedence level for contexts that provide their own grouping, like list brackets or
39+
# function call parentheses. This ensures parentheses will never be added for the direct children of these nodes.
40+
# NOTE: `ruff_python_formatter::expression::parentheses`'s state machine would be more robust
41+
# but would introduce significant complexity.
42+
NONE = auto()
43+
44+
YIELD = auto() # `yield`, `yield from`
45+
ASSIGN = auto() # `target := expr`
46+
STARRED = auto() # `*expr` (omitted by Python docs, see ruff impl)
47+
LAMBDA = auto()
48+
IF_ELSE = auto() # `expr if cond else expr`
49+
OR = auto()
50+
AND = auto()
51+
NOT = auto()
52+
COMP_MEMB_ID = auto() # `<`, `<=`, `>`, `>=`, `!=`, `==`, `in`, `not in`, `is`, `is not`
53+
BIT_OR = auto() # `|`
54+
BIT_XOR = auto() # `^`
55+
BIT_AND = auto() # `&`
56+
LEFT_RIGHT_SHIFT = auto() # `<<`, `>>`
57+
ADD_SUB = auto() # `+`, `-`
58+
MUL_DIV_REMAIN = auto() # `*`, `@`, `/`, `//`, `%`
59+
POS_NEG_BIT_NOT = auto() # `+x`, `-x`, `~x`
60+
EXPONENT = auto() # `**`
61+
AWAIT = auto()
62+
CALL_ATTRIBUTE = auto() # `x[index]`, `x[index:index]`, `x(arguments...)`, `x.attribute`
63+
ATOMIC = auto() # `(expressions...)`, `[expressions...]`, `{key: value...}`, `{expressions...}`
64+
65+
66+
def _yield(
67+
element: str | Expr | tuple[str | Expr, ...],
68+
*,
69+
flat: bool = True,
70+
is_left: bool = False,
71+
outer_precedence: _OperatorPrecedence = _OperatorPrecedence.ATOMIC,
72+
) -> Iterator[str | Expr]:
73+
if isinstance(element, Expr):
74+
element_precedence = _get_precedence(element)
75+
needs_parens = False
76+
# Lower inner precedence, e.g. `(a + b) * c`, `+(10) < *(11)`.
77+
if element_precedence < outer_precedence:
78+
needs_parens = True
79+
elif element_precedence == outer_precedence:
80+
# Right-association, e.g. parenthesize left-hand side in `(a ** b) ** c`, (a if b else c) if d else e
81+
is_right_assoc = isinstance(element, ExprIfExp) or (
82+
isinstance(element, ExprBinOp) and element.operator == "**"
83+
)
84+
if is_right_assoc:
85+
if is_left:
86+
needs_parens = True
87+
# Left-association, e.g. parenthesize right-hand side in `a - (b - c)`.
88+
elif isinstance(element, (ExprBinOp, ExprBoolOp)) and not is_left:
89+
needs_parens = True
90+
91+
if needs_parens:
92+
yield "("
93+
if flat:
94+
yield from element.iterate(flat=True)
95+
else:
96+
yield element
97+
yield ")"
98+
elif flat:
99+
yield from element.iterate(flat=True)
100+
else:
101+
yield element
32102
elif isinstance(element, tuple):
33103
for elem in element:
34-
yield from _yield(elem, flat=flat)
35-
elif flat:
36-
yield from element.iterate(flat=True)
104+
yield from _yield(elem, flat=flat, outer_precedence=outer_precedence, is_left=is_left)
37105
else:
38106
yield element
39107

@@ -44,14 +112,21 @@ def _join(
44112
*,
45113
flat: bool = True,
46114
) -> Iterator[str | Expr]:
115+
"""Apply a separator between elements.
116+
117+
The caller is assumed to provide their own grouping
118+
(e.g. lists, tuples, slice) and will prevent parentheses from being added.
119+
"""
47120
it = iter(elements)
48121
try:
49-
yield from _yield(next(it), flat=flat)
122+
# Since we are in a sequence, don't parenthesize items.
123+
# Avoids [a + b, c + d] being serialized as [(a + b), (c + d)]
124+
yield from _yield(next(it), flat=flat, outer_precedence=_OperatorPrecedence.NONE)
50125
except StopIteration:
51126
return
52127
for element in it:
53-
yield from _yield(joint, flat=flat)
54-
yield from _yield(element, flat=flat)
128+
yield from _yield(joint, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
129+
yield from _yield(element, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
55130

56131

57132
def _field_as_dict(
@@ -184,7 +259,11 @@ class ExprAttribute(Expr):
184259
"""The different parts of the dotted chain."""
185260

186261
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
187-
yield from _join(self.values, ".", flat=flat)
262+
precedence = _get_precedence(self)
263+
yield from _yield(self.values[0], flat=flat, outer_precedence=precedence, is_left=True)
264+
for value in self.values[1:]:
265+
yield "."
266+
yield from _yield(value, flat=flat, outer_precedence=precedence)
188267

189268
def append(self, value: ExprName) -> None:
190269
"""Append a name to this attribute.
@@ -232,9 +311,14 @@ class ExprBinOp(Expr):
232311
"""Right part."""
233312

234313
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
235-
yield from _yield(self.left, flat=flat)
314+
precedence = _get_precedence(self)
315+
right_precedence = precedence
316+
if self.operator == "**" and isinstance(self.right, ExprUnaryOp):
317+
# Unary operators on the right have higher precedence, e.g. `a ** -b`.
318+
right_precedence = _OperatorPrecedence(precedence - 1)
319+
yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
236320
yield f" {self.operator} "
237-
yield from _yield(self.right, flat=flat)
321+
yield from _yield(self.right, flat=flat, outer_precedence=right_precedence, is_left=False)
238322

239323

240324
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -248,7 +332,12 @@ class ExprBoolOp(Expr):
248332
"""Operands."""
249333

250334
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
251-
yield from _join(self.values, f" {self.operator} ", flat=flat)
335+
precedence = _get_precedence(self)
336+
it = iter(self.values)
337+
yield from _yield(next(it), flat=flat, outer_precedence=precedence, is_left=True)
338+
for value in it:
339+
yield f" {self.operator} "
340+
yield from _yield(value, flat=flat, outer_precedence=precedence, is_left=False)
252341

253342

254343
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -267,7 +356,7 @@ def canonical_path(self) -> str:
267356
return self.function.canonical_path
268357

269358
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
270-
yield from _yield(self.function, flat=flat)
359+
yield from _yield(self.function, flat=flat, outer_precedence=_get_precedence(self))
271360
yield "("
272361
yield from _join(self.arguments, ", ", flat=flat)
273362
yield ")"
@@ -286,9 +375,11 @@ class ExprCompare(Expr):
286375
"""Things compared."""
287376

288377
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
289-
yield from _yield(self.left, flat=flat)
290-
yield " "
291-
yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat)
378+
precedence = _get_precedence(self)
379+
yield from _yield(self.left, flat=flat, outer_precedence=precedence, is_left=True)
380+
for op, comp in zip(self.operators, self.comparators):
381+
yield f" {op} "
382+
yield from _yield(comp, flat=flat, outer_precedence=precedence)
292383

293384

294385
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -396,7 +487,8 @@ class ExprFormatted(Expr):
396487

397488
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
398489
yield "{"
399-
yield from _yield(self.value, flat=flat)
490+
# Prevent parentheses from being added, avoiding `{(1 + 1)}`
491+
yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
400492
yield "}"
401493

402494

@@ -429,11 +521,21 @@ class ExprIfExp(Expr):
429521
"""Other expression."""
430522

431523
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
432-
yield from _yield(self.body, flat=flat)
524+
precedence = _get_precedence(self)
525+
yield from _yield(self.body, flat=flat, outer_precedence=precedence, is_left=True)
433526
yield " if "
434-
yield from _yield(self.test, flat=flat)
527+
# If the test itself is another if/else, its precedence is the same, which will not give
528+
# a parenthesis: force it.
529+
test_outer_precedence = _OperatorPrecedence(precedence + 1)
530+
yield from _yield(self.test, flat=flat, outer_precedence=test_outer_precedence)
435531
yield " else "
436-
yield from _yield(self.orelse, flat=flat)
532+
# If/else is right associative. For example, a nested if/else
533+
# `a if b else c if d else e` is effectively `a if b else (c if d else e)`, so produce a
534+
# flattened version without parentheses.
535+
if isinstance(self.orelse, ExprIfExp):
536+
yield from self.orelse.iterate(flat=flat)
537+
else:
538+
yield from _yield(self.orelse, flat=flat, outer_precedence=precedence, is_left=False)
437539

438540

439541
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -557,7 +659,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
557659
if index < length:
558660
yield ", "
559661
yield ": "
560-
yield from _yield(self.body, flat=flat)
662+
# Body of lambda should not have parentheses, avoiding `lambda: a.b`
663+
yield from _yield(self.body, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
561664

562665

563666
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -685,11 +788,9 @@ class ExprNamedExpr(Expr):
685788
"""Value."""
686789

687790
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
688-
yield "("
689791
yield from _yield(self.target, flat=flat)
690792
yield " := "
691793
yield from _yield(self.value, flat=flat)
692-
yield ")"
693794

694795

695796
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -773,9 +874,10 @@ class ExprSubscript(Expr):
773874
"""Slice part."""
774875

775876
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
776-
yield from _yield(self.left, flat=flat)
877+
yield from _yield(self.left, flat=flat, outer_precedence=_get_precedence(self))
777878
yield "["
778-
yield from _yield(self.slice, flat=flat)
879+
# Prevent parentheses from being added, avoiding `a[(b)]`
880+
yield from _yield(self.slice, flat=flat, outer_precedence=_OperatorPrecedence.NONE)
779881
yield "]"
780882

781883
@property
@@ -825,7 +927,9 @@ class ExprUnaryOp(Expr):
825927

826928
def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
827929
yield self.operator
828-
yield from _yield(self.value, flat=flat)
930+
if self.operator == "not":
931+
yield " "
932+
yield from _yield(self.value, flat=flat, outer_precedence=_get_precedence(self))
829933

830934

831935
# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line.
@@ -858,7 +962,7 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
858962

859963
_unary_op_map = {
860964
ast.Invert: "~",
861-
ast.Not: "not ",
965+
ast.Not: "not",
862966
ast.UAdd: "+",
863967
ast.USub: "-",
864968
}
@@ -897,6 +1001,65 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]:
8971001
ast.NotIn: "not in",
8981002
}
8991003

1004+
# TODO: Support `ast.Await`.
1005+
_precedence_map = {
1006+
# Literals and names.
1007+
ExprName: lambda _: _OperatorPrecedence.ATOMIC,
1008+
ExprConstant: lambda _: _OperatorPrecedence.ATOMIC,
1009+
ExprJoinedStr: lambda _: _OperatorPrecedence.ATOMIC,
1010+
ExprFormatted: lambda _: _OperatorPrecedence.ATOMIC,
1011+
# Container displays.
1012+
ExprList: lambda _: _OperatorPrecedence.ATOMIC,
1013+
ExprTuple: lambda _: _OperatorPrecedence.ATOMIC,
1014+
ExprSet: lambda _: _OperatorPrecedence.ATOMIC,
1015+
ExprDict: lambda _: _OperatorPrecedence.ATOMIC,
1016+
# Comprehensions are self-contained units that produce a container.
1017+
ExprListComp: lambda _: _OperatorPrecedence.ATOMIC,
1018+
ExprSetComp: lambda _: _OperatorPrecedence.ATOMIC,
1019+
ExprDictComp: lambda _: _OperatorPrecedence.ATOMIC,
1020+
ExprAttribute: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1021+
ExprSubscript: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1022+
ExprCall: lambda _: _OperatorPrecedence.CALL_ATTRIBUTE,
1023+
ExprUnaryOp: lambda e: {"not": _OperatorPrecedence.NOT}.get(e.operator, _OperatorPrecedence.POS_NEG_BIT_NOT),
1024+
ExprBinOp: lambda e: {
1025+
"**": _OperatorPrecedence.EXPONENT,
1026+
"*": _OperatorPrecedence.MUL_DIV_REMAIN,
1027+
"@": _OperatorPrecedence.MUL_DIV_REMAIN,
1028+
"/": _OperatorPrecedence.MUL_DIV_REMAIN,
1029+
"//": _OperatorPrecedence.MUL_DIV_REMAIN,
1030+
"%": _OperatorPrecedence.MUL_DIV_REMAIN,
1031+
"+": _OperatorPrecedence.ADD_SUB,
1032+
"-": _OperatorPrecedence.ADD_SUB,
1033+
"<<": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1034+
">>": _OperatorPrecedence.LEFT_RIGHT_SHIFT,
1035+
"&": _OperatorPrecedence.BIT_AND,
1036+
"^": _OperatorPrecedence.BIT_XOR,
1037+
"|": _OperatorPrecedence.BIT_OR,
1038+
}[e.operator],
1039+
ExprBoolOp: lambda e: {"and": _OperatorPrecedence.AND, "or": _OperatorPrecedence.OR}[e.operator],
1040+
ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID,
1041+
ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE,
1042+
ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN,
1043+
ExprLambda: lambda _: _OperatorPrecedence.LAMBDA,
1044+
# NOTE: Ruff categorizes as atomic, but `(a for a in b).c` implies its less than `CALL_ATTRIBUTE`.
1045+
ExprGeneratorExp: lambda _: _OperatorPrecedence.LAMBDA,
1046+
ExprVarPositional: lambda _: _OperatorPrecedence.STARRED,
1047+
ExprVarKeyword: lambda _: _OperatorPrecedence.STARRED,
1048+
ExprYield: lambda _: _OperatorPrecedence.YIELD,
1049+
ExprYieldFrom: lambda _: _OperatorPrecedence.YIELD,
1050+
# These are not standalone, they appear in specific contexts where precendence is not a concern.
1051+
# NOTE: `for ... in ... if` part, not the whole `[...]`.
1052+
ExprComprehension: lambda _: _OperatorPrecedence.NONE,
1053+
ExprExtSlice: lambda _: _OperatorPrecedence.NONE,
1054+
ExprKeyword: lambda _: _OperatorPrecedence.NONE,
1055+
ExprParameter: lambda _: _OperatorPrecedence.NONE,
1056+
ExprSlice: lambda _: _OperatorPrecedence.NONE,
1057+
}
1058+
1059+
1060+
def _get_precedence(expr: Expr) -> _OperatorPrecedence:
1061+
return _precedence_map.get(type(expr), lambda _: _OperatorPrecedence.NONE)(expr)
1062+
9001063

9011064
def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr:
9021065
left = _build(node.value, parent, **kwargs)
@@ -1113,7 +1276,7 @@ def _build_subscript(
11131276
"typing_extensions.Literal",
11141277
}:
11151278
literal_strings = True
1116-
slice = _build(
1279+
slice_expr = _build(
11171280
node.slice,
11181281
parent,
11191282
parse_strings=True,
@@ -1122,8 +1285,8 @@ def _build_subscript(
11221285
**kwargs,
11231286
)
11241287
else:
1125-
slice = _build(node.slice, parent, in_subscript=True, **kwargs)
1126-
return ExprSubscript(left, slice)
1288+
slice_expr = _build(node.slice, parent, in_subscript=True, **kwargs)
1289+
return ExprSubscript(left, slice_expr)
11271290

11281291

11291292
def _build_tuple(

0 commit comments

Comments
 (0)