summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rwxr-xr-xpycodestyle.py106
-rw-r--r--testsuite/E10.py4
-rw-r--r--testsuite/E11.py3
-rw-r--r--testsuite/E22.py13
-rw-r--r--testsuite/E30.py8
-rw-r--r--testsuite/E40.py15
-rw-r--r--testsuite/E72.py3
-rw-r--r--testsuite/E90.py2
-rw-r--r--testsuite/W19.py38
-rw-r--r--testsuite/python3.py7
-rw-r--r--testsuite/python38.py15
-rw-r--r--testsuite/test_api.py4
-rw-r--r--testsuite/test_shell.py2
-rw-r--r--tox.ini4
15 files changed, 172 insertions, 54 deletions
diff --git a/.travis.yml b/.travis.yml
index ce324be..696c1c0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,8 @@ matrix:
env: TOXENV=py36
- python: 3.7
env: TOXENV=py37
+ - python: 3.8
+ env: TOXENV=py38
- python: pypy2.7-6.0
env: TOXENV=pypy
- python: pypy3.5-6.0
diff --git a/pycodestyle.py b/pycodestyle.py
index 0ecd7fd..705ba25 100755
--- a/pycodestyle.py
+++ b/pycodestyle.py
@@ -113,14 +113,17 @@ PyCF_ONLY_AST = 1024
SINGLETONS = frozenset(['False', 'None', 'True'])
KEYWORDS = frozenset(keyword.kwlist + ['print', 'async']) - SINGLETONS
UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-'])
-ARITHMETIC_OP = frozenset(['**', '*', '/', '//', '+', '-'])
+ARITHMETIC_OP = frozenset(['**', '*', '/', '//', '+', '-', '@'])
WS_OPTIONAL_OPERATORS = ARITHMETIC_OP.union(['^', '&', '|', '<<', '>>', '%'])
# Warn for -> function annotation operator in py3.5+ (issue 803)
FUNCTION_RETURN_ANNOTATION_OP = ['->'] if sys.version_info >= (3, 5) else []
+ASSIGNMENT_EXPRESSION_OP = [':='] if sys.version_info >= (3, 8) else []
WS_NEEDED_OPERATORS = frozenset([
'**=', '*=', '/=', '//=', '+=', '-=', '!=', '<>', '<', '>',
- '%=', '^=', '&=', '|=', '==', '<=', '>=', '<<=', '>>=', '='] +
- FUNCTION_RETURN_ANNOTATION_OP)
+ '%=', '^=', '&=', '|=', '==', '<=', '>=', '<<=', '>>=', '=',
+ 'and', 'in', 'is', 'or'] +
+ FUNCTION_RETURN_ANNOTATION_OP +
+ ASSIGNMENT_EXPRESSION_OP)
WHITESPACE = frozenset(' \t')
NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE])
SKIP_TOKENS = NEWLINE.union([tokenize.INDENT, tokenize.DEDENT])
@@ -133,12 +136,12 @@ RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,')
RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,.*,\s*\w+\s*$')
ERRORCODE_REGEX = re.compile(r'\b[A-Z]\d{3}\b')
DOCSTRING_REGEX = re.compile(r'u?r?["\']')
-EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[\[({] | [\]}),;:]')
+EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[\[({] | [\]}),;]| :(?!=)')
WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)')
COMPARE_SINGLETON_REGEX = re.compile(r'(\bNone|\bFalse|\bTrue)?\s*([=!]=)'
r'\s*(?(1)|(None|False|True))\b')
COMPARE_NEGATIVE_REGEX = re.compile(r'\b(not)\s+[^][)(}{ ]+\s+(in|is)\s')
-COMPARE_TYPE_REGEX = re.compile(r'(?:[=!]=|is(?:\s+not)?)\s*type(?:s.\w+Type'
+COMPARE_TYPE_REGEX = re.compile(r'(?:[=!]=|is(?:\s+not)?)\s+type(?:s.\w+Type'
r'|\s*\(\s*([^)]*[^ )])\s*\))')
KEYWORD_REGEX = re.compile(r'(\s*)\b(?:%s)\b(\s*)' % r'|'.join(KEYWORDS))
OPERATOR_REGEX = re.compile(r'(?:[^,\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)')
@@ -357,14 +360,15 @@ def blank_lines(logical_line, blank_lines, indent_level, line_number,
):
yield 0, "E303 too many blank lines (%d)" % blank_lines
elif STARTSWITH_TOP_LEVEL_REGEX.match(logical_line):
- # If this is a one-liner (i.e. the next line is not more
- # indented), and the previous line is also not deeper
- # (it would be better to check if the previous line is part
- # of another def/class at the same level), don't require blank
- # lines around this.
+ # If this is a one-liner (i.e. this is not a decorator and the
+ # next line is not more indented), and the previous line is also
+ # not deeper (it would be better to check if the previous line
+ # is part of another def/class at the same level), don't require
+ # blank lines around this.
prev_line = lines[line_number - 2] if line_number >= 2 else ''
next_line = lines[line_number] if line_number < len(lines) else ''
- if (expand_indent(prev_line) <= indent_level and
+ if (not logical_line.startswith("@") and
+ expand_indent(prev_line) <= indent_level and
expand_indent(next_line) <= indent_level):
return
if indent_level:
@@ -493,13 +497,16 @@ def missing_whitespace(logical_line):
line = logical_line
for index in range(len(line) - 1):
char = line[index]
- if char in ',;:' and line[index + 1] not in WHITESPACE:
+ next_char = line[index + 1]
+ if char in ',;:' and next_char not in WHITESPACE:
before = line[:index]
if char == ':' and before.count('[') > before.count(']') and \
before.rfind('{') < before.rfind('['):
continue # Slice syntax, no space required
- if char == ',' and line[index + 1] == ')':
+ if char == ',' and next_char == ')':
continue # Allow tuple with only one element: (3,)
+ if char == ':' and next_char == '=' and sys.version_info >= (3, 8):
+ continue # Allow assignment expression
yield index, "E231 missing whitespace after '%s'" % char
@@ -534,9 +541,11 @@ def indentation(logical_line, previous_logical, indent_char,
elif not indent_expect and indent_level > previous_indent_level:
yield 0, tmpl % (3 + c, "unexpected indentation")
- expected_indent_level = previous_indent_level + 4
- if indent_expect and indent_level > expected_indent_level:
- yield 0, tmpl % (7, 'over-indented')
+ if indent_expect:
+ expected_indent_amount = 8 if indent_char == '\t' else 4
+ expected_indent_level = previous_indent_level + expected_indent_amount
+ if indent_level > expected_indent_level:
+ yield 0, tmpl % (7, 'over-indented')
@register_check
@@ -822,6 +831,7 @@ def missing_whitespace_around_operator(logical_line, tokens):
E225: submitted +=1
E225: x = x /2 - 1
E225: z = x **y
+ E225: z = 1and 1
E226: c = (a+b) * (a-b)
E226: hypot2 = x*x + y*y
E227: c = a|b
@@ -831,6 +841,7 @@ def missing_whitespace_around_operator(logical_line, tokens):
need_space = False
prev_type = tokenize.OP
prev_text = prev_end = None
+ operator_types = (tokenize.OP, tokenize.NAME)
for token_type, text, start, end, line in tokens:
if token_type in SKIP_COMMENTS:
continue
@@ -849,6 +860,10 @@ def missing_whitespace_around_operator(logical_line, tokens):
# Tolerate the "<>" operator, even if running Python 3
# Deal with Python 3's annotated return value "->"
pass
+ elif prev_text == '/' and text == ',':
+ # Tolerate the "/" operator in function definition
+ # For more info see PEP570
+ pass
else:
if need_space is True or need_space[1]:
# A needed trailing space was not found
@@ -862,7 +877,7 @@ def missing_whitespace_around_operator(logical_line, tokens):
yield (need_space[0], "%s missing whitespace "
"around %s operator" % (code, optype))
need_space = False
- elif token_type == tokenize.OP and prev_end is not None:
+ elif token_type in operator_types and prev_end is not None:
if text == '=' and parens:
# Allow keyword args or defaults: foo(bar=None).
pass
@@ -1067,7 +1082,8 @@ def module_imports_on_top_of_file(
line = line[1:]
return line and (line[0] == '"' or line[0] == "'")
- allowed_try_keywords = ('try', 'except', 'else', 'finally')
+ allowed_keywords = (
+ 'try', 'except', 'else', 'finally', 'with', 'if', 'elif')
if indent_level: # Allow imports in conditional statement/function
return
@@ -1081,9 +1097,9 @@ def module_imports_on_top_of_file(
yield 0, "E402 module level import not at top of file"
elif re.match(DUNDER_REGEX, line):
return
- elif any(line.startswith(kw) for kw in allowed_try_keywords):
- # Allow try, except, else, finally keywords intermixed with
- # imports in order to support conditional importing
+ elif any(line.startswith(kw) for kw in allowed_keywords):
+ # Allow certain keywords intermixed with imports in order to
+ # support conditional or filtered importing
return
elif is_string_literal(line):
# The first literal is a docstring, allow it. Otherwise, report
@@ -1135,7 +1151,9 @@ def compound_statements(logical_line):
update_counts(line[prev_found:found], counts)
if ((counts['{'] <= counts['}'] and # {'a': 1} (dict)
counts['['] <= counts[']'] and # [1:2] (slice)
- counts['('] <= counts[')'])): # (annotation)
+ counts['('] <= counts[')']) and # (annotation)
+ not (sys.version_info >= (3, 8) and
+ line[found + 1] == '=')): # assignment expression
lambda_kw = LAMBDA_REGEX.search(line, 0, found)
if lambda_kw:
before = line[:lambda_kw.start()].rstrip()
@@ -1199,13 +1217,16 @@ def explicit_line_join(logical_line, tokens):
parens -= 1
+_SYMBOLIC_OPS = frozenset("()[]{},:.;@=%~") | frozenset(("...",))
+
+
def _is_binary_operator(token_type, text):
is_op_token = token_type == tokenize.OP
is_conjunction = text in ['and', 'or']
# NOTE(sigmavirus24): Previously the not_a_symbol check was executed
# conditionally. Since it is now *always* executed, text may be
# None. In that case we get a TypeError for `text not in str`.
- not_a_symbol = text and text not in "()[]{},:.;@=%~"
+ not_a_symbol = text and text not in _SYMBOLIC_OPS
# The % character is strictly speaking a binary operator, but the
# common usage seems to be to put it next to the format parameters,
# after a line break.
@@ -1418,28 +1439,58 @@ def ambiguous_identifier(logical_line, tokens):
Variables can be bound in several other contexts, including class
and function definitions, 'global' and 'nonlocal' statements,
- exception handlers, and 'with' statements.
+ exception handlers, and 'with' and 'for' statements.
+ In addition, we have a special handling for function parameters.
Okay: except AttributeError as o:
Okay: with lock as L:
+ Okay: foo(l=12)
+ Okay: for a in foo(l=12):
E741: except AttributeError as O:
E741: with lock as l:
E741: global I
E741: nonlocal l
+ E741: def foo(l):
+ E741: def foo(l=12):
+ E741: l = foo(l=12)
+ E741: for l in range(10):
E742: class I(object):
E743: def l(x):
"""
+ is_func_def = False # Set to true if 'def' is found
+ parameter_parentheses_level = 0
idents_to_avoid = ('l', 'O', 'I')
prev_type, prev_text, prev_start, prev_end, __ = tokens[0]
for token_type, text, start, end, line in tokens[1:]:
ident = pos = None
+ # find function definitions
+ if prev_text == 'def':
+ is_func_def = True
+ # update parameter parentheses level
+ if parameter_parentheses_level == 0 and \
+ prev_type == tokenize.NAME and \
+ token_type == tokenize.OP and text == '(':
+ parameter_parentheses_level = 1
+ elif parameter_parentheses_level > 0 and \
+ token_type == tokenize.OP:
+ if text == '(':
+ parameter_parentheses_level += 1
+ elif text == ')':
+ parameter_parentheses_level -= 1
# identifiers on the lhs of an assignment operator
- if token_type == tokenize.OP and '=' in text:
+ if token_type == tokenize.OP and '=' in text and \
+ parameter_parentheses_level == 0:
if prev_text in idents_to_avoid:
ident = prev_text
pos = prev_start
- # identifiers bound to values with 'as', 'global', or 'nonlocal'
- if prev_text in ('as', 'global', 'nonlocal'):
+ # identifiers bound to values with 'as', 'for',
+ # 'global', or 'nonlocal'
+ if prev_text in ('as', 'for', 'global', 'nonlocal'):
+ if text in idents_to_avoid:
+ ident = text
+ pos = start
+ # function parameter definitions
+ if is_func_def:
if text in idents_to_avoid:
ident = text
pos = start
@@ -1451,6 +1502,7 @@ def ambiguous_identifier(logical_line, tokens):
yield start, "E743 ambiguous function definition '%s'" % text
if ident:
yield pos, "E741 ambiguous variable name '%s'" % ident
+ prev_type = token_type
prev_text = text
prev_start = start
diff --git a/testsuite/E10.py b/testsuite/E10.py
index 08b03a0..7b42594 100644
--- a/testsuite/E10.py
+++ b/testsuite/E10.py
@@ -3,7 +3,7 @@ for a in 'abc':
for b in 'xyz':
print a # indented with 8 spaces
print b # indented with 1 tab
-#: E101 E117 E122 W191 W191
+#: E101 E122 W191 W191
if True:
pass
@@ -27,7 +27,7 @@ class TestP4Poller(unittest.TestCase):
pass
#
-#: E101 E117 W191 W191
+#: E101 W191 W191
if True:
foo(1,
2)
diff --git a/testsuite/E11.py b/testsuite/E11.py
index c7b11d1..ef87d92 100644
--- a/testsuite/E11.py
+++ b/testsuite/E11.py
@@ -37,3 +37,6 @@ def start(self):
#: E117
def start():
print
+#: E117 W191
+def start():
+ print
diff --git a/testsuite/E22.py b/testsuite/E22.py
index ee9dc74..7ea2792 100644
--- a/testsuite/E22.py
+++ b/testsuite/E22.py
@@ -76,6 +76,18 @@ _1kB = _1MB>> 10
i=i+ 1
#: E225 E225
i=i +1
+#: E225
+i = 1and 1
+#: E225
+i = 1or 0
+#: E225
+1is 1
+#: E225
+1in []
+#: E225
+i = 1 @2
+#: E225
+i = 1@ 2
#: E225 E226
i=i+1
#: E225 E226
@@ -142,6 +154,7 @@ if not -5 < x < +5:
print >>sys.stderr, "x is out of range."
print >> sys.stdout, "x is an integer."
x = x / 2 - 1
+x = 1 @ 2
if alpha[:-i]:
*a, b = (1, 2, 3)
diff --git a/testsuite/E30.py b/testsuite/E30.py
index 320b2a1..15fbdf3 100644
--- a/testsuite/E30.py
+++ b/testsuite/E30.py
@@ -181,3 +181,11 @@ def foo():
def bar(): pass
def baz():
pass
+#: E302:5:1
+def f():
+ pass
+
+# wat
+@hi
+def g():
+ pass
diff --git a/testsuite/E40.py b/testsuite/E40.py
index f9a18fc..6c71fa2 100644
--- a/testsuite/E40.py
+++ b/testsuite/E40.py
@@ -34,6 +34,21 @@ finally:
print('made attempt to import foo')
import bar
+#: Okay
+with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", DeprecationWarning)
+ import foo
+
+import bar
+#: Okay
+if False:
+ import foo
+elif not True:
+ import bar
+else:
+ import mwahaha
+
+import bar
#: E402
VERSION = '1.2.3'
diff --git a/testsuite/E72.py b/testsuite/E72.py
index c18527f..a60d892 100644
--- a/testsuite/E72.py
+++ b/testsuite/E72.py
@@ -49,6 +49,9 @@ if isinstance(res, types.MethodType):
pass
if type(a) != type(b) or type(a) == type(ccc):
pass
+#: Okay
+def func_histype(a, b, c):
+ pass
#: E722
try:
pass
diff --git a/testsuite/E90.py b/testsuite/E90.py
index 3fd2b32..2c18e9a 100644
--- a/testsuite/E90.py
+++ b/testsuite/E90.py
@@ -2,7 +2,7 @@
}
#: E901
= [x
-#: E901 E101 E117 W191
+#: E901 E101 W191
while True:
try:
pass
diff --git a/testsuite/W19.py b/testsuite/W19.py
index d77bc9e..ed69e2b 100644
--- a/testsuite/W19.py
+++ b/testsuite/W19.py
@@ -1,4 +1,4 @@
-#: E117 W191
+#: W191
if False:
print # indented with 1 tab
#:
@@ -7,30 +7,30 @@ if False:
#: W191
y = x == 2 \
or x == 3
-#: E101 E117 W191 W504
+#: E101 W191 W504
if (
x == (
3
) or
y == 4):
pass
-#: E101 E117 W191
+#: E101 W191
if x == 2 \
or y > 1 \
or x == 3:
pass
-#: E101 E117 W191
+#: E101 W191
if x == 2 \
or y > 1 \
or x == 3:
pass
#:
-#: E101 E117 W191 W504
+#: E101 W191 W504
if (foo == bar and
baz == frop):
pass
-#: E101 E117 W191 W504
+#: E101 W191 W504
if (
foo == bar and
baz == frop
@@ -38,25 +38,25 @@ if (
pass
#:
-#: E101 E101 E117 W191 W191
+#: E101 E101 W191 W191
if start[1] > end_col and not (
over_indent == 4 and indent_next):
return(0, "E121 continuation line over-"
"indented for visual indent")
#:
-#: E101 E117 W191
+#: E101 W191
def long_function_name(
var_one, var_two, var_three,
var_four):
print(var_one)
-#: E101 E117 W191 W504
+#: E101 W191 W504
if ((row < 0 or self.moduleCount <= row or
col < 0 or self.moduleCount <= col)):
raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
-#: E101 E101 E101 E101 E117 W191 W191 W191 W191 W191 W191
+#: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
if bar:
return(
start, 'E121 lines starting with a '
@@ -65,35 +65,35 @@ if bar:
"bracket's line"
)
#
-#: E101 E117 W191 W504
+#: E101 W191 W504
# you want vertical alignment, so use a parens
if ((foo.bar("baz") and
foo.bar("frop")
)):
print "yes"
-#: E101 E117 W191 W504
+#: E101 W191 W504
# also ok, but starting to look like LISP
if ((foo.bar("baz") and
foo.bar("frop"))):
print "yes"
-#: E101 E117 W191 W504
+#: E101 W191 W504
if (a == 2 or
b == "abc def ghi"
"jkl mno"):
return True
-#: E101 E117 W191 W504
+#: E101 W191 W504
if (a == 2 or
b == """abc def ghi
jkl mno"""):
return True
-#: W191:2:1 W191:3:1 E101:3:2 E117
+#: W191:2:1 W191:3:1 E101:3:2
if length > options.max_line_length:
return options.max_line_length, \
"E501 line too long (%d characters)" % length
#
-#: E101 E117 W191 W191 W504
+#: E101 W191 W191 W504
if os.path.exists(os.path.join(path, PEP8_BIN)):
cmd = ([os.path.join(path, PEP8_BIN)] +
self._pep8_options(targetfile))
@@ -119,19 +119,19 @@ or long long long long long long long long long long long long long long long lo
even though the noqa comment is not immediately after the string
''' + foo # noqa
#
-#: E101 E117 W191
+#: E101 W191
if foo is None and bar is "frop" and \
blah == 'yeah':
blah = 'yeahnah'
#
-#: E117 W191 W191 W191
+#: W191 W191 W191
if True:
foo(
1,
2)
-#: E117 W191 W191 W191 W191 W191
+#: W191 W191 W191 W191 W191
def test_keys(self):
"""areas.json - All regions are accounted for."""
expected = set([
diff --git a/testsuite/python3.py b/testsuite/python3.py
index be7c58f..709695a 100644
--- a/testsuite/python3.py
+++ b/testsuite/python3.py
@@ -37,3 +37,10 @@ class Class:
def m(self):
xs: List[int] = []
+
+
+# Used to trigger W504
+def f(
+ x: str = ...
+):
+ ...
diff --git a/testsuite/python38.py b/testsuite/python38.py
new file mode 100644
index 0000000..5f62a3f
--- /dev/null
+++ b/testsuite/python38.py
@@ -0,0 +1,15 @@
+#: Okay
+def f(a, /, b):
+ pass
+#: Okay
+if x := 1:
+ print(x)
+if m and (token := m.group(1)):
+ pass
+stuff = [[y := f(x), x / y] for x in range(5)]
+#: E225:1:5
+if x:= 1:
+ pass
+#: E225:1:18
+if False or (x :=1):
+ pass
diff --git a/testsuite/test_api.py b/testsuite/test_api.py
index ddc28b8..c69e54d 100644
--- a/testsuite/test_api.py
+++ b/testsuite/test_api.py
@@ -125,7 +125,7 @@ class APITestCase(unittest.TestCase):
report = pycodestyle.StyleGuide().check_files([E11])
stdout = sys.stdout.getvalue().splitlines()
self.assertEqual(len(stdout), report.total_errors)
- self.assertEqual(report.total_errors, 20)
+ self.assertEqual(report.total_errors, 24)
self.assertFalse(sys.stderr)
self.reset()
@@ -133,7 +133,7 @@ class APITestCase(unittest.TestCase):
report = pycodestyle.StyleGuide(paths=[E11]).check_files()
stdout = sys.stdout.getvalue().splitlines()
self.assertEqual(len(stdout), report.total_errors)
- self.assertEqual(report.total_errors, 20)
+ self.assertEqual(report.total_errors, 24)
self.assertFalse(sys.stderr)
self.reset()
diff --git a/testsuite/test_shell.py b/testsuite/test_shell.py
index e4d0871..3878194 100644
--- a/testsuite/test_shell.py
+++ b/testsuite/test_shell.py
@@ -77,7 +77,7 @@ class ShellTestCase(unittest.TestCase):
stdout = stdout.splitlines()
self.assertEqual(errcode, 1)
self.assertFalse(stderr)
- self.assertEqual(len(stdout), 20)
+ self.assertEqual(len(stdout), 24)
for line, num, col in zip(stdout, (3, 6, 6, 9, 12), (3, 6, 6, 1, 5)):
path, x, y, msg = line.split(':')
self.assertTrue(path.endswith(E11))
diff --git a/tox.ini b/tox.ini
index c263284..fc42fc1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,10 @@
-# Tox (https://testrun.org/tox/latest/) is a tool for running tests
+# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
-envlist = py27, py34, py35, py36, py37, pypy, pypy3, jython
+envlist = py27, py34, py35, py36, py37, py38, pypy, pypy3, jython
skipsdist = True
skip_missing_interpreters = True