Skip to content

Commit 32230e6

Browse files
fix: bug where the doublestar operation had inconsistent formatting. (#4154)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 7edb50f commit 32230e6

File tree

6 files changed

+132
-26
lines changed

6 files changed

+132
-26
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
- Move the `hug_parens_with_braces_and_square_brackets` feature to the unstable style
1818
due to an outstanding crash and proposed formatting tweaks (#4198)
19+
- Fixed a bug where base expressions caused inconsistent formatting of \*\* in tenary
20+
expression (#4154)
1921
- Checking for newline before adding one on docstring that is almost at the line limit
2022
(#4185)
2123

docs/the_black_code_style/future_style.md

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Currently, the following features are included in the preview style:
2828
longer normalized
2929
- `typed_params_trailing_comma`: consistently add trailing commas to typed function
3030
parameters
31+
- `is_simple_lookup_for_doublestar_expression`: fix line length computation for certain
32+
expressions that involve the power operator
3133
- `docstring_check_for_newline`: checks if there is a newline before the terminating
3234
quotes of a docstring
3335

src/black/mode.py

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class Preview(Enum):
177177
wrap_long_dict_values_in_parens = auto()
178178
multiline_string_handling = auto()
179179
typed_params_trailing_comma = auto()
180+
is_simple_lookup_for_doublestar_expression = auto()
180181
docstring_check_for_newline = auto()
181182

182183

src/black/resources/black.schema.json

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"wrap_long_dict_values_in_parens",
8787
"multiline_string_handling",
8888
"typed_params_trailing_comma",
89+
"is_simple_lookup_for_doublestar_expression",
8990
"docstring_check_for_newline"
9091
]
9192
},

src/black/trans.py

+112-26
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
from black.comments import contains_pragma_comment
3131
from black.lines import Line, append_leaves
32-
from black.mode import Feature, Mode
32+
from black.mode import Feature, Mode, Preview
3333
from black.nodes import (
3434
CLOSING_BRACKETS,
3535
OPENING_BRACKETS,
@@ -94,43 +94,36 @@ def hug_power_op(
9494
else:
9595
raise CannotTransform("No doublestar token was found in the line.")
9696

97-
def is_simple_lookup(index: int, step: Literal[1, -1]) -> bool:
97+
def is_simple_lookup(index: int, kind: Literal[1, -1]) -> bool:
9898
# Brackets and parentheses indicate calls, subscripts, etc. ...
9999
# basically stuff that doesn't count as "simple". Only a NAME lookup
100100
# or dotted lookup (eg. NAME.NAME) is OK.
101-
if step == -1:
102-
disallowed = {token.RPAR, token.RSQB}
103-
else:
104-
disallowed = {token.LPAR, token.LSQB}
105-
106-
while 0 <= index < len(line.leaves):
107-
current = line.leaves[index]
108-
if current.type in disallowed:
109-
return False
110-
if current.type not in {token.NAME, token.DOT} or current.value == "for":
111-
# If the current token isn't disallowed, we'll assume this is simple as
112-
# only the disallowed tokens are semantically attached to this lookup
113-
# expression we're checking. Also, stop early if we hit the 'for' bit
114-
# of a comprehension.
115-
return True
101+
if Preview.is_simple_lookup_for_doublestar_expression not in mode:
102+
return original_is_simple_lookup_func(line, index, kind)
116103

117-
index += step
118-
119-
return True
104+
else:
105+
if kind == -1:
106+
return handle_is_simple_look_up_prev(
107+
line, index, {token.RPAR, token.RSQB}
108+
)
109+
else:
110+
return handle_is_simple_lookup_forward(
111+
line, index, {token.LPAR, token.LSQB}
112+
)
120113

121-
def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
114+
def is_simple_operand(index: int, kind: Literal[1, -1]) -> bool:
122115
# An operand is considered "simple" if's a NAME, a numeric CONSTANT, a simple
123116
# lookup (see above), with or without a preceding unary operator.
124117
start = line.leaves[index]
125118
if start.type in {token.NAME, token.NUMBER}:
126-
return is_simple_lookup(index, step=(1 if kind == "exponent" else -1))
119+
return is_simple_lookup(index, kind)
127120

128121
if start.type in {token.PLUS, token.MINUS, token.TILDE}:
129122
if line.leaves[index + 1].type in {token.NAME, token.NUMBER}:
130-
# step is always one as bases with a preceding unary op will be checked
123+
# kind is always one as bases with a preceding unary op will be checked
131124
# for simplicity starting from the next token (so it'll hit the check
132125
# above).
133-
return is_simple_lookup(index + 1, step=1)
126+
return is_simple_lookup(index + 1, kind=1)
134127

135128
return False
136129

@@ -145,9 +138,9 @@ def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
145138
should_hug = (
146139
(0 < idx < len(line.leaves) - 1)
147140
and leaf.type == token.DOUBLESTAR
148-
and is_simple_operand(idx - 1, kind="base")
141+
and is_simple_operand(idx - 1, kind=-1)
149142
and line.leaves[idx - 1].value != "lambda"
150-
and is_simple_operand(idx + 1, kind="exponent")
143+
and is_simple_operand(idx + 1, kind=1)
151144
)
152145
if should_hug:
153146
new_leaf.prefix = ""
@@ -162,6 +155,99 @@ def is_simple_operand(index: int, kind: Literal["base", "exponent"]) -> bool:
162155
yield new_line
163156

164157

158+
def original_is_simple_lookup_func(
159+
line: Line, index: int, step: Literal[1, -1]
160+
) -> bool:
161+
if step == -1:
162+
disallowed = {token.RPAR, token.RSQB}
163+
else:
164+
disallowed = {token.LPAR, token.LSQB}
165+
166+
while 0 <= index < len(line.leaves):
167+
current = line.leaves[index]
168+
if current.type in disallowed:
169+
return False
170+
if current.type not in {token.NAME, token.DOT} or current.value == "for":
171+
# If the current token isn't disallowed, we'll assume this is
172+
# simple as only the disallowed tokens are semantically
173+
# attached to this lookup expression we're checking. Also,
174+
# stop early if we hit the 'for' bit of a comprehension.
175+
return True
176+
177+
index += step
178+
179+
return True
180+
181+
182+
def handle_is_simple_look_up_prev(line: Line, index: int, disallowed: Set[int]) -> bool:
183+
"""
184+
Handling the determination of is_simple_lookup for the lines prior to the doublestar
185+
token. This is required because of the need to isolate the chained expression
186+
to determine the bracket or parenthesis belong to the single expression.
187+
"""
188+
contains_disallowed = False
189+
chain = []
190+
191+
while 0 <= index < len(line.leaves):
192+
current = line.leaves[index]
193+
chain.append(current)
194+
if not contains_disallowed and current.type in disallowed:
195+
contains_disallowed = True
196+
if not is_expression_chained(chain):
197+
return not contains_disallowed
198+
199+
index -= 1
200+
201+
return True
202+
203+
204+
def handle_is_simple_lookup_forward(
205+
line: Line, index: int, disallowed: Set[int]
206+
) -> bool:
207+
"""
208+
Handling decision is_simple_lookup for the lines behind the doublestar token.
209+
This function is simplified to keep consistent with the prior logic and the forward
210+
case are more straightforward and do not need to care about chained expressions.
211+
"""
212+
while 0 <= index < len(line.leaves):
213+
current = line.leaves[index]
214+
if current.type in disallowed:
215+
return False
216+
if current.type not in {token.NAME, token.DOT} or (
217+
current.type == token.NAME and current.value == "for"
218+
):
219+
# If the current token isn't disallowed, we'll assume this is simple as
220+
# only the disallowed tokens are semantically attached to this lookup
221+
# expression we're checking. Also, stop early if we hit the 'for' bit
222+
# of a comprehension.
223+
return True
224+
225+
index += 1
226+
227+
return True
228+
229+
230+
def is_expression_chained(chained_leaves: List[Leaf]) -> bool:
231+
"""
232+
Function to determine if the variable is a chained call.
233+
(e.g., foo.lookup, foo().lookup, (foo.lookup())) will be recognized as chained call)
234+
"""
235+
if len(chained_leaves) < 2:
236+
return True
237+
238+
current_leaf = chained_leaves[-1]
239+
past_leaf = chained_leaves[-2]
240+
241+
if past_leaf.type == token.NAME:
242+
return current_leaf.type in {token.DOT}
243+
elif past_leaf.type in {token.RPAR, token.RSQB}:
244+
return current_leaf.type in {token.RSQB, token.RPAR}
245+
elif past_leaf.type in {token.LPAR, token.LSQB}:
246+
return current_leaf.type in {token.NAME, token.LPAR, token.LSQB}
247+
else:
248+
return False
249+
250+
165251
class StringTransformer(ABC):
166252
"""
167253
An implementation of the Transformer protocol that relies on its
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# flags: --preview
2+
m2 = None if not isinstance(dist, Normal) else m** 2 + s * 2
3+
m3 = None if not isinstance(dist, Normal) else m ** 2 + s * 2
4+
m4 = None if not isinstance(dist, Normal) else m**2 + s * 2
5+
m5 = obj.method(another_obj.method()).attribute **2
6+
m6 = None if ... else m**2 + s**2
7+
8+
9+
# output
10+
m2 = None if not isinstance(dist, Normal) else m**2 + s * 2
11+
m3 = None if not isinstance(dist, Normal) else m**2 + s * 2
12+
m4 = None if not isinstance(dist, Normal) else m**2 + s * 2
13+
m5 = obj.method(another_obj.method()).attribute ** 2
14+
m6 = None if ... else m**2 + s**2

0 commit comments

Comments
 (0)