Skip to content

Commit 5343100

Browse files
authored
Merge pull request #502 from sigmavirus24/add-W504
Add W504 for line breaks before binary operators
2 parents 4cee4a5 + 147c399 commit 5343100

File tree

8 files changed

+129
-81
lines changed

8 files changed

+129
-81
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ UNRELEASED
66

77
New checks:
88

9+
* Add W504 warning for checking that a break doesn't happen after a binary
10+
operator. This check is ignored by default
911
* Add W605 warning for invalid escape sequences in string literals
1012

1113
2.3.1 (2017-01-31)

pycodestyle.py

Lines changed: 83 additions & 30 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,34 +1135,26 @@ def explicit_line_join(logical_line, tokens):
11351135
parens -= 1
11361136

11371137

1138-
@register_check
1139-
def break_around_binary_operator(logical_line, tokens):
1140-
r"""
1141-
Avoid breaks before binary operators.
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)
11421149

1143-
The preferred place to break around a binary operator is after the
1144-
operator, not before it.
11451150

1146-
W503: (width == 0\n + height == 0)
1147-
W503: (width == 0\n and height == 0)
1151+
def _break_around_binary_operators(tokens):
1152+
"""Private function to reduce duplication.
11481153
1149-
Okay: (width == 0 +\n height == 0)
1150-
Okay: foo(\n -x)
1151-
Okay: foo(x\n [])
1152-
Okay: x = '''\n''' + ''
1153-
Okay: foo(x,\n -y)
1154-
Okay: foo(x, # comment\n -y)
1155-
Okay: var = (1 &\n ~2)
1156-
Okay: var = (1 /\n -2)
1157-
Okay: var = (1 +\n -1 +\n -2)
1154+
This factors out the shared details between
1155+
:func:`break_before_binary_operator` and
1156+
:func:`break_after_binary_operator`.
11581157
"""
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 "()[]{},:.;@=%~")
1165-
11661158
line_break = False
11671159
unary_context = True
11681160
# Previous non-newline token types and text
@@ -1174,17 +1166,78 @@ def is_binary_operator(token_type, text):
11741166
if ('\n' in text or '\r' in text) and token_type != tokenize.STRING:
11751167
line_break = True
11761168
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"
1169+
yield (token_type, text, previous_token_type, previous_text,
1170+
line_break, unary_context, start)
11821171
unary_context = text in '([{,;'
11831172
line_break = False
11841173
previous_token_type = token_type
11851174
previous_text = text
11861175

11871176

1177+
@register_check
1178+
def break_before_binary_operator(logical_line, tokens):
1179+
r"""
1180+
Avoid breaks before binary operators.
1181+
1182+
The preferred place to break around a binary operator is after the
1183+
operator, not before it.
1184+
1185+
W503: (width == 0\n + height == 0)
1186+
W503: (width == 0\n and height == 0)
1187+
W503: var = (1\n & ~2)
1188+
W503: var = (1\n / -2)
1189+
W503: var = (1\n + -1\n + -2)
1190+
1191+
Okay: foo(\n -x)
1192+
Okay: foo(x\n [])
1193+
Okay: x = '''\n''' + ''
1194+
Okay: foo(x,\n -y)
1195+
Okay: foo(x, # comment\n -y)
1196+
"""
1197+
for context in _break_around_binary_operators(tokens):
1198+
(token_type, text, previous_token_type, previous_text,
1199+
line_break, unary_context, start) = context
1200+
if (_is_binary_operator(token_type, text) and line_break and
1201+
not unary_context and
1202+
not _is_binary_operator(previous_token_type,
1203+
previous_text)):
1204+
yield start, "W503 line break before binary operator"
1205+
1206+
1207+
@register_check
1208+
def break_after_binary_operator(logical_line, tokens):
1209+
r"""
1210+
Avoid breaks after binary operators.
1211+
1212+
The preferred place to break around a binary operator is before the
1213+
operator, not after it.
1214+
1215+
W504: (width == 0 +\n height == 0)
1216+
W504: (width == 0 and\n height == 0)
1217+
W504: var = (1 &\n ~2)
1218+
1219+
Okay: foo(\n -x)
1220+
Okay: foo(x\n [])
1221+
Okay: x = '''\n''' + ''
1222+
Okay: x = '' + '''\n'''
1223+
Okay: foo(x,\n -y)
1224+
Okay: foo(x, # comment\n -y)
1225+
1226+
The following should be W504 but unary_context is tricky with these
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) and
1234+
line_break and
1235+
not unary_context and
1236+
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"
1239+
1240+
11881241
@register_check
11891242
def comparison_to_singleton(logical_line, noqa):
11901243
r"""Comparison to singletons should use "is" or "is not".

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ license_file = LICENSE
66

77
[pycodestyle]
88
select =
9-
ignore = E226,E24
9+
ignore = E226,E24,W504
1010
max_line_length = 79

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: 17 additions & 23 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 \
@@ -17,15 +16,14 @@
1716
or y > 1 \
1817
or x == 3:
1918
pass
20-
21-
22-
if (foo == bar and
23-
baz == frop):
19+
#: W503
20+
if (foo == bar
21+
and baz == frop):
2422
pass
25-
23+
#: W503
2624
if (
27-
foo == bar and
28-
baz == frop
25+
foo == bar
26+
and baz == frop
2927
):
3028
pass
3129

@@ -109,7 +107,7 @@
109107
'BBB' \
110108
'iii' \
111109
'CCC'
112-
110+
#: W504 W504
113111
abricot = (3 +
114112
4 +
115113
5 + 6)
@@ -138,8 +136,7 @@ def long_function_name(
138136
var_one, var_two, var_three,
139137
var_four):
140138
print(var_one)
141-
142-
139+
#: W504
143140
if ((row < 0 or self.moduleCount <= row or
144141
col < 0 or self.moduleCount <= col)):
145142
raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
@@ -184,23 +181,23 @@ def long_function_name(
184181
"to match that of the opening "
185182
"bracket's line"
186183
)
187-
#
184+
#: W504
188185
# you want vertical alignment, so use a parens
189186
if ((foo.bar("baz") and
190187
foo.bar("frop")
191188
)):
192189
print "yes"
193-
190+
#: W504
194191
# also ok, but starting to look like LISP
195192
if ((foo.bar("baz") and
196193
foo.bar("frop"))):
197194
print "yes"
198-
195+
#: W504
199196
if (a == 2 or
200197
b == "abc def ghi"
201198
"jkl mno"):
202199
return True
203-
200+
#: W504
204201
if (a == 2 or
205202
b == """abc def ghi
206203
jkl mno"""):
@@ -224,22 +221,19 @@ def long_function_name(
224221
print('%-7d %s per second (%d total)' % (
225222
options.counters[key] / elapsed, key,
226223
options.counters[key]))
227-
228-
224+
#: W504
229225
if os.path.exists(os.path.join(path, PEP8_BIN)):
230226
cmd = ([os.path.join(path, PEP8_BIN)] +
231227
self._pep8_options(targetfile))
232-
233-
228+
#: W504
234229
fixed = (re.sub(r'\t+', ' ', target[c::-1], 1)[::-1] +
235230
target[c + 1:])
236-
231+
#: W504
237232
fixed = (
238233
re.sub(r'\t+', ' ', target[c::-1], 1)[::-1] +
239234
target[c + 1:]
240235
)
241-
242-
236+
#: W504
243237
if foo is None and bar is "frop" and \
244238
blah == 'yeah':
245239
blah = 'yeahnah'

testsuite/W19.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#: W191
88
y = x == 2 \
99
or x == 3
10-
#: E101 W191
10+
#: E101 W191 W504
1111
if (
1212
x == (
1313
3
@@ -26,11 +26,11 @@
2626
pass
2727
#:
2828

29-
#: E101 W191
29+
#: E101 W191 W504
3030
if (foo == bar and
3131
baz == frop):
3232
pass
33-
#: E101 W191
33+
#: E101 W191 W504
3434
if (
3535
foo == bar and
3636
baz == frop
@@ -52,7 +52,7 @@ def long_function_name(
5252
var_one, var_two, var_three,
5353
var_four):
5454
print(var_one)
55-
#: E101 W191
55+
#: E101 W191 W504
5656
if ((row < 0 or self.moduleCount <= row or
5757
col < 0 or self.moduleCount <= col)):
5858
raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
@@ -65,23 +65,23 @@ def long_function_name(
6565
"bracket's line"
6666
)
6767
#
68-
#: E101 W191
68+
#: E101 W191 W504
6969
# you want vertical alignment, so use a parens
7070
if ((foo.bar("baz") and
7171
foo.bar("frop")
7272
)):
7373
print "yes"
74-
#: E101 W191
74+
#: E101 W191 W504
7575
# also ok, but starting to look like LISP
7676
if ((foo.bar("baz") and
7777
foo.bar("frop"))):
7878
print "yes"
79-
#: E101 W191
79+
#: E101 W191 W504
8080
if (a == 2 or
8181
b == "abc def ghi"
8282
"jkl mno"):
8383
return True
84-
#: E101 W191
84+
#: E101 W191 W504
8585
if (a == 2 or
8686
b == """abc def ghi
8787
jkl mno"""):
@@ -93,7 +93,7 @@ def long_function_name(
9393

9494

9595
#
96-
#: E101 W191 W191
96+
#: E101 W191 W191 W504
9797
if os.path.exists(os.path.join(path, PEP8_BIN)):
9898
cmd = ([os.path.join(path, PEP8_BIN)] +
9999
self._pep8_options(targetfile))

0 commit comments

Comments
 (0)