diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rwxr-xr-x | pycodestyle.py | 106 | ||||
-rw-r--r-- | testsuite/E10.py | 4 | ||||
-rw-r--r-- | testsuite/E11.py | 3 | ||||
-rw-r--r-- | testsuite/E22.py | 13 | ||||
-rw-r--r-- | testsuite/E30.py | 8 | ||||
-rw-r--r-- | testsuite/E40.py | 15 | ||||
-rw-r--r-- | testsuite/E72.py | 3 | ||||
-rw-r--r-- | testsuite/E90.py | 2 | ||||
-rw-r--r-- | testsuite/W19.py | 38 | ||||
-rw-r--r-- | testsuite/python3.py | 7 | ||||
-rw-r--r-- | testsuite/python38.py | 15 | ||||
-rw-r--r-- | testsuite/test_api.py | 4 | ||||
-rw-r--r-- | testsuite/test_shell.py | 2 | ||||
-rw-r--r-- | tox.ini | 4 |
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)) @@ -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 |