Skip to content

Commit 8f3aebd

Browse files
committed
Add W504 for line breaks before binary operators
This flips the W503 rule to enforce line breaks before binary operators. Related #498
1 parent 4cee4a5 commit 8f3aebd

3 files changed

Lines changed: 100 additions & 49 deletions

File tree

pycodestyle.py

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation.
8080
__version__ = '2.3.1'
8181

8282
DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox'
83-
DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503'
83+
DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504'
8484
try:
8585
if sys.platform == 'win32':
8686
USER_CONFIG = os.path.expanduser(r'~\.pycodestyle')
@@ -1135,8 +1135,47 @@ def explicit_line_join(logical_line, tokens):
11351135
parens -= 1
11361136

11371137

1138+
def _is_binary_operator(token_type, text):
1139+
is_op_token = token_type == tokenize.OP
1140+
is_conjunction = text in ['and', 'or']
1141+
# NOTE(sigmavirus24): Previously the not_a_symbol check was executed
1142+
# conditionally. Since it is now *always* executed, text may be None.
1143+
# In that case we get a TypeError for `text not in str`.
1144+
not_a_symbol = text and text not in "()[]{},:.;@=%~"
1145+
# The % character is strictly speaking a binary operator, but the
1146+
# common usage seems to be to put it next to the format parameters,
1147+
# after a line break.
1148+
return ((is_op_token or is_conjunction) and not_a_symbol)
1149+
1150+
1151+
def _break_around_binary_operators(tokens):
1152+
"""Private function to reduce duplication.
1153+
1154+
This factors out the shared details between
1155+
:func:`break_before_binary_operator` and
1156+
:func:`break_after_binary_operator`.
1157+
"""
1158+
line_break = False
1159+
unary_context = True
1160+
# Previous non-newline token types and text
1161+
previous_token_type = None
1162+
previous_text = None
1163+
for token_type, text, start, end, line in tokens:
1164+
if token_type == tokenize.COMMENT:
1165+
continue
1166+
if ('\n' in text or '\r' in text) and token_type != tokenize.STRING:
1167+
line_break = True
1168+
else:
1169+
yield (token_type, text, previous_token_type, previous_text,
1170+
line_break, unary_context, start)
1171+
unary_context = text in '([{,;'
1172+
line_break = False
1173+
previous_token_type = token_type
1174+
previous_text = text
1175+
1176+
11381177
@register_check
1139-
def break_around_binary_operator(logical_line, tokens):
1178+
def break_before_binary_operator(logical_line, tokens):
11401179
r"""
11411180
Avoid breaks before binary operators.
11421181
@@ -1156,33 +1195,47 @@ def break_around_binary_operator(logical_line, tokens):
11561195
Okay: var = (1 /\n -2)
11571196
Okay: var = (1 +\n -1 +\n -2)
11581197
"""
1159-
def is_binary_operator(token_type, text):
1160-
# The % character is strictly speaking a binary operator, but the
1161-
# common usage seems to be to put it next to the format parameters,
1162-
# after a line break.
1163-
return ((token_type == tokenize.OP or text in ['and', 'or']) and
1164-
text not in "()[]{},:.;@=%~")
1198+
for context in _break_around_binary_operators(tokens):
1199+
(token_type, text, previous_token_type, previous_text,
1200+
line_break, unary_context, start) = context
1201+
if (_is_binary_operator(token_type, text) and line_break and
1202+
not unary_context and
1203+
not _is_binary_operator(previous_token_type,
1204+
previous_text)):
1205+
yield start, "W503 line break before binary operator"
11651206

1166-
line_break = False
1167-
unary_context = True
1168-
# Previous non-newline token types and text
1169-
previous_token_type = None
1170-
previous_text = None
1171-
for token_type, text, start, end, line in tokens:
1172-
if token_type == tokenize.COMMENT:
1173-
continue
1174-
if ('\n' in text or '\r' in text) and token_type != tokenize.STRING:
1175-
line_break = True
1176-
else:
1177-
if (is_binary_operator(token_type, text) and line_break and
1178-
not unary_context and
1179-
not is_binary_operator(previous_token_type,
1180-
previous_text)):
1181-
yield start, "W503 line break before binary operator"
1182-
unary_context = text in '([{,;'
1183-
line_break = False
1184-
previous_token_type = token_type
1185-
previous_text = text
1207+
1208+
@register_check
1209+
def break_after_binary_operator(logical_line, tokens):
1210+
r"""
1211+
Avoid breaks after binary operators.
1212+
1213+
The preferred place to break around a binary operator is after the
1214+
operator, not before it.
1215+
1216+
W504: (width == 0 +\n height == 0)
1217+
W504: (width == 0 and\n height == 0)
1218+
1219+
Okay: (width == 0\n + height == 0)
1220+
Okay: foo(\n -x)
1221+
Okay: foo(x\n [])
1222+
Okay: x = '''\n''' + ''
1223+
Okay: x = '' + '''\n'''
1224+
Okay: foo(x,\n -y)
1225+
Okay: foo(x, # comment\n -y)
1226+
Okay: var = (1\n & ~2)
1227+
Okay: var = (1\n / -2)
1228+
Okay: var = (1\n + -1\n + -2)
1229+
"""
1230+
for context in _break_around_binary_operators(tokens):
1231+
(token_type, text, previous_token_type, previous_text,
1232+
line_break, unary_context, start) = context
1233+
if (_is_binary_operator(previous_token_type, previous_text)
1234+
and line_break
1235+
and not unary_context
1236+
and not _is_binary_operator(token_type, text)):
1237+
error_pos = (start[0] - 1, start[1])
1238+
yield error_pos, "W504 line break after binary operator"
11861239

11871240

11881241
@register_check

testsuite/E12.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
#: E124
2121
a = (123,
2222
)
23-
#: E129
24-
if (row < 0 or self.moduleCount <= row or
25-
col < 0 or self.moduleCount <= col):
23+
#: E129 W503
24+
if (row < 0 or self.moduleCount <= row
25+
or col < 0 or self.moduleCount <= col):
2626
raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
2727
#: E126
2828
print "E126", (
@@ -195,9 +195,9 @@ def qualify_by_address(
195195
self, cr, uid, ids, context=None,
196196
params_to_check=frozenset(QUALIF_BY_ADDRESS_PARAM)):
197197
""" This gets called by the web server """
198-
#: E129
199-
if (a == 2 or
200-
b == "abc def ghi"
198+
#: E129 W503
199+
if (a == 2
200+
or b == "abc def ghi"
201201
"jkl mno"):
202202
return True
203203
#:
@@ -225,22 +225,21 @@ def qualify_by_address(
225225
eat_a_dict_a_day({
226226
"foo": "bar",
227227
})
228-
#: E126
228+
#: E126 W503
229229
if (
230230
x == (
231231
3
232-
) or
233-
y == 4):
232+
)
233+
or y == 4):
234234
pass
235-
#: E126
235+
#: E126 W503 W503
236236
if (
237237
x == (
238238
3
239-
) or
240-
x == (
241-
3
242-
) or
243-
y == 4):
239+
)
240+
or x == (
241+
3)
242+
or y == 4):
244243
pass
245244
#: E131
246245
troublesome_hash = {

testsuite/E12not.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
if (
22
x == (
33
3
4-
) or
5-
y == 4):
4+
) or y == 4):
65
pass
76

87
y = x == 2 \
@@ -19,13 +18,13 @@
1918
pass
2019

2120

22-
if (foo == bar and
23-
baz == frop):
21+
if (foo == bar
22+
and baz == frop):
2423
pass
2524

2625
if (
27-
foo == bar and
28-
baz == frop
26+
foo == bar
27+
and baz == frop
2928
):
3029
pass
3130

0 commit comments

Comments
 (0)