Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix false alarm in E203 rule (issue #373, black incompatibility) #914

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion pycodestyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ def extraneous_whitespace(logical_line):
- Immediately inside parentheses, brackets or braces.
- Immediately before a comma, semicolon, or colon.

Exceptions:
- When the colon acts as a slice, the rule of binary operators
applies and we should have the same amount of space on either side
- When the colon acts as a slice but a parameter is omitted, then
the space is omitted

Okay: spam(ham[1], {eggs: 2})
E201: spam( ham[1], {eggs: 2})
E201: spam(ham[ 1], {eggs: 2})
Expand All @@ -421,10 +427,45 @@ def extraneous_whitespace(logical_line):
E202: spam(ham[1 ], {eggs: 2})
E202: spam(ham[1], {eggs: 2 })

Okay: ham[8 : 2]
Okay: ham[: 2]
E203: if x == 4: print x, y; x, y = y , x
E203: if x == 4: print x, y ; x, y = y, x
E203: if x == 4 : print x, y; x, y = y, x
"""

def is_a_slice(line, space_pos):
"""Check if the colon after an extra space acts as a slice

Return True if the colon is directly contained within
square brackets.
"""
# list of found '[({' as tuples "(char, position)"
parentheses_brackets_braces = list()
for i in range(space_pos):
c = line[i]
if c in '[({':
# add it to the stack
parentheses_brackets_braces.append((c, i))
elif c in '])}':
# unstack last item and check consistency
last_opened = parentheses_brackets_braces.pop()[0]
expected_close = {'{': '}', '(': ')', '[': ']'}[last_opened]
if c != expected_close: # invalid Python code
return False
is_within_brackets = (len(parentheses_brackets_braces) > 0 and
parentheses_brackets_braces[-1][0] == '[')

is_lambda_expr = False
if is_within_brackets:
# check for a lambda expression nested in brackets
if space_pos > 6:
last_opened = parentheses_brackets_braces[-1]
between_bracket_colon = line[last_opened[1] + 1: space_pos]
is_lambda_expr = 'lambda' in between_bracket_colon

return (is_within_brackets and not is_lambda_expr)

line = logical_line
for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line):
text = match.group()
Expand All @@ -433,7 +474,8 @@ def extraneous_whitespace(logical_line):
if text == char + ' ':
# assert char in '([{'
yield found + 1, "E201 whitespace after '%s'" % char
elif line[found - 1] != ',':
elif (line[found - 1] != ',' and
not (char == ':' and is_a_slice(line, found))):
Comment on lines +477 to +478
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
elif (line[found - 1] != ',' and
not (char == ':' and is_a_slice(line, found))):
elif (line[found - 1] != ','
and not (char == ':' and is_a_slice(line, found))):

I was under the impression that 'pythonic' style puts the binary operator after a line break, not before.

--- Personal preference / opinion only below this line. Disregard at your pleasure ---

I personally also prefer to format multi-line conditions like this:

        elif (
            line[found - 1] != ','
            and not (char == ':' and is_a_slice(line, found))
        ):

because it puts co-ordinate conditions in a visually co-ordinate structure, and also has dedicated lines to mark the bounds of the compound condition. (i.e. the elif ( and ): lines, visually positioned to encapsulate the conditions and indicate that they're subordinate to the elif construct itself. It does take up a lot more vertical space though, so I can understand why some might dislike that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hoylemd I agree and prefer this coding style as well.
Yet W503 in TravisCI prevents me of doing so :

pycodestyle.py:489:13: W503 line break before binary operator
pycodestyle.py:512:13: W503 line break before binary operator

If @s-weigand suggestion to ignore W503 was accepted then we could proceed :)

Copy link
Contributor

@FichteFoll FichteFoll Aug 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that 'pythonic' style puts the binary operator after a line break, not before.

This was changed only a few years back (roughly when W504 was introduced and both were added to the default ignore list). pycodestyle itself does not use this style of line breaks before binary operators and you should always adhere to the project's style.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for the precision
Then I guess the PR is ok for review

code = ('E202' if char in '}])' else 'E203') # if char in ',;:'
yield found, "%s whitespace before '%s'" % (code, char)

Expand Down
12 changes: 12 additions & 0 deletions testsuite/E20.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,22 @@
if x == 4:
print x, y
x, y = y , x
#: E203:1:37
foo[idxs[2 : 6] : spam(ham[1], {eggs : a[2 : 4]})]
#: E203:1:8
[lambda : foo]
#: E203:1:13
ham[lambda x : foo]
#: Okay
if x == 4:
print x, y
x, y = y, x
a[b1, :] == a[b1, ...]
b = a[:, b1]
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]
foo[idxs[2 : 6] : spam(ham[1], {eggs: a[2 : 4]})]
[lambda: foo]
ham[lambda x: foo]
#: