1010import sys
1111from dataclasses import dataclass
1212from dataclasses import fields as getfields
13+ from enum import IntEnum , auto
1314from functools import partial
14- from itertools import zip_longest
1515from typing import TYPE_CHECKING , Any , Callable
1616
1717from _griffe .agents .nodes .parameters import get_parameters
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
57132def _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
9011064def _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
11291292def _build_tuple (
0 commit comments