--- a/Plugins/CheckerPlugins/CodeStyleChecker/pep8.py Sun Sep 04 14:34:56 2016 +0200 +++ b/Plugins/CheckerPlugins/CodeStyleChecker/pep8.py Sun Sep 04 16:50:23 2016 +0200 @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# pep8.py - Check Python source code formatting, according to PEP 8 +# pycodestyle.py - Check Python source code formatting, according to PEP 8 # Copyright (C) 2006-2009 Johann C. Rocholl <johann@rocholl.net> # Copyright (C) 2009-2014 Florent Xicluna <florent.xicluna@gmail.com> # Copyright (C) 2014-2016 Ian Lee <ianlee1521@gmail.com> @@ -30,10 +30,10 @@ Check Python source code formatting, according to PEP 8. For usage and a list of options, try this: -$ python pep8.py -h +$ python pycodestyle.py -h This program and its regression test suite live here: -https://github.com/pycqa/pep8 +https://github.com/pycqa/pycodestyle Groups of errors and warnings: E errors @@ -60,38 +60,38 @@ # Copyright (c) 2011 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> # -import os -import sys -import re -import time import inspect import keyword +import os +import re +import sys +import time import tokenize -import ast +##import ast +from fnmatch import fnmatch from optparse import OptionParser -from fnmatch import fnmatch try: from configparser import RawConfigParser from io import TextIOWrapper except ImportError: from ConfigParser import RawConfigParser # __IGNORE_WARNING__ -__version__ = '1.7.0' +__version__ = '2.1.0.dev0' DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox' -DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704' +DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503' try: if sys.platform == 'win32': - USER_CONFIG = os.path.expanduser(r'~\.pep8') + USER_CONFIG = os.path.expanduser(r'~\.pycodestyle') else: USER_CONFIG = os.path.join( os.getenv('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'), - 'pep8' + 'pycodestyle' ) except ImportError: USER_CONFIG = None -PROJECT_CONFIG = ('setup.cfg', 'tox.ini', '.pep8') +PROJECT_CONFIG = ('setup.cfg', 'tox.ini') TESTSUITE_PATH = os.path.join(os.path.dirname(__file__), 'testsuite') MAX_LINE_LENGTH = 79 REPORT_FORMAT = { @@ -99,6 +99,7 @@ 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', } +PyCF_ONLY_AST = 1024 SINGLETONS = frozenset(['False', 'None', 'True']) KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) @@ -208,7 +209,7 @@ return len(physical_line), "W292 no newline at end of file" -def maximum_line_length(physical_line, max_line_length, multiline): +def maximum_line_length(physical_line, max_line_length, multiline, noqa): r"""Limit all lines to a maximum of 79 characters. There are still many devices around that are limited to 80 character @@ -222,7 +223,7 @@ """ line = physical_line.rstrip() length = len(line) - if length > max_line_length and not noqa(line): + if length > max_line_length and not noqa: # Special case for long URLs in multi-line docstrings or comments, # but still report the error when the 72 first chars are whitespaces. chunks = line.split() @@ -248,7 +249,9 @@ def blank_lines(logical_line, blank_lines, indent_level, line_number, - blank_before, previous_logical, previous_indent_level): + blank_before, previous_logical, + previous_unindented_logical_line, previous_indent_level, + lines): r"""Separate top-level function and class definitions with two blank lines. Method definitions inside a class are separated by a single blank line. @@ -260,13 +263,16 @@ Use blank lines in functions, sparingly, to indicate logical sections. Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\nasync def b():\n pass Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass E301: class Foo:\n b = 0\n def bar():\n pass E302: def a():\n pass\n\ndef b(n):\n pass + E302: def a():\n pass\n\nasync def b(n):\n pass E303: def a():\n pass\n\n\n\ndef b(n):\n pass E303: def a():\n\n\n\n pass E304: @decorator\n\ndef a():\n pass + E305: def a():\n pass\na() """ if line_number < 3 and not previous_logical: return # Don't expect blank lines before the first line @@ -275,13 +281,30 @@ yield 0, "E304 blank lines found after function decorator" elif blank_lines > 2 or (indent_level and blank_lines == 2): yield 0, "E303 too many blank lines (%d)", blank_lines - elif logical_line.startswith(('def ', 'class ', '@')): + elif logical_line.startswith(('def ', 'async def ', 'class ', '@')): if indent_level: if not (blank_before or previous_indent_level < indent_level or DOCSTRING_REGEX.match(previous_logical)): - yield 0, "E301 expected 1 blank line, found 0" + ancestor_level = indent_level + nested = False + # Search backwards for a def ancestor or tree root (top level). + for line in lines[line_number - 2::-1]: + if line.strip() and expand_indent(line) < ancestor_level: + ancestor_level = expand_indent(line) + nested = line.lstrip().startswith('def ') + if nested or ancestor_level == 0: + break + if nested: + yield 0, "E306 expected 1 blank line before a " \ + "nested definition, found 0" + else: + yield 0, "E301 expected 1 blank line, found 0" elif blank_before != 2: yield 0, "E302 expected 2 blank lines, found %d", blank_before + elif (logical_line and not indent_level and blank_before != 2 and + previous_unindented_logical_line.startswith(('def', 'class'))): + yield 0, "E305 expected 2 blank lines after " \ + "class or function definition, found %d", blank_before def extraneous_whitespace(logical_line): @@ -339,6 +362,23 @@ yield match.start(2), "E271 multiple spaces after keyword" +def missing_whitespace_after_import_keyword(logical_line): + r"""Multiple imports in form from x import (a, b, c) should have space + between import statement and parenthesised name list. + + Okay: from foo import (bar, baz) + E275: from foo import(bar, baz) + E275: from importable.module import(bar, baz) + """ + line = logical_line + indicator = ' import(' + if line.startswith('from '): + found = line.find(indicator) + if -1 < found: + pos = found + len(indicator) - 1 + yield pos, "E275 missing whitespace after keyword" + + def missing_whitespace(logical_line): r"""Each comma, semicolon or colon should be followed by whitespace. @@ -579,7 +619,7 @@ break assert len(indent) == depth + 1 if start[1] not in indent_chances: - # allow to line up tokens + # allow lining up tokens indent_chances[start[1]] = text last_token_multiline = (start[0] != end[0]) @@ -773,6 +813,7 @@ Okay: boolean(a <= b) Okay: boolean(a >= b) Okay: def foo(arg: int = 42): + Okay: async def foo(arg: int = 42): E251: def complex(real, imag = 0.0): E251: return magic(r = real, i = imag) @@ -781,7 +822,7 @@ no_space = False prev_end = None annotated_func_arg = False - in_def = logical_line.startswith('def') + in_def = logical_line.startswith(('def', 'async def')) message = "E251 unexpected spaces around keyword / parameter equals" for token_type, text, start, end, line in tokens: if token_type == tokenize.NL: @@ -791,9 +832,9 @@ if start != prev_end: yield (prev_end, message) if token_type == tokenize.OP: - if text == '(': + if text in '([': parens += 1 - elif text == ')': + elif text in ')]': parens -= 1 elif in_def and text == ':' and parens == 1: annotated_func_arg = True @@ -851,7 +892,7 @@ def imports_on_separate_lines(logical_line): - r"""Imports should usually be on separate lines. + r"""Place imports on separate lines. Okay: import os\nimport sys E401: import sys, os @@ -871,8 +912,10 @@ def module_imports_on_top_of_file( logical_line, indent_level, checker_state, noqa): - r"""Imports are always put at the top of the file, just after any module - comments and docstrings, and before module globals and constants. + r"""Place imports at the top of the file. + + Always put imports at the top of the file, just after any module comments + and docstrings, and before module globals and constants. Okay: import os Okay: # this is a comment\nimport os @@ -945,27 +988,33 @@ E702: do_one(); do_two(); do_three() E703: do_four(); # useless semicolon E704: def f(x): return 2*x + E705: async def f(x): return 2*x E731: f = lambda x: 2*x """ line = logical_line last_char = len(line) - 1 found = line.find(':') + prev_found = 0 + counts = dict((char, 0) for char in '{}[]()') while -1 < found < last_char: - before = line[:found] - if ((before.count('{') <= before.count('}') and # {'a': 1} (dict) - before.count('[') <= before.count(']') and # [1:2] (slice) - before.count('(') <= before.count(')'))): # (annotation) - lambda_kw = LAMBDA_REGEX.search(before) + update_counts(line[prev_found:found], counts) + if ((counts['{'] <= counts['}'] and # {'a': 1} (dict) + counts['['] <= counts[']'] and # [1:2] (slice) + counts['('] <= counts[')'])): # (annotation) + lambda_kw = LAMBDA_REGEX.search(line, 0, found) if lambda_kw: before = line[:lambda_kw.start()].rstrip() if before[-1:] == '=' and isidentifier(before[:-1].strip()): yield 0, ("E731 do not assign a lambda expression, use a " "def") break - if before.startswith('def '): + if line.startswith('def '): yield 0, "E704 multiple statements on one line (def)" + elif line.startswith('async def '): + yield 0, "E705 multiple statements on one line (async def)" else: yield found, "E701 multiple statements on one line (colon)" + prev_found = found found = line.find(':', found + 1) found = line.find(';') while -1 < found: @@ -1031,16 +1080,22 @@ Okay: x = '''\n''' + '' Okay: foo(x,\n -y) Okay: foo(x, # comment\n -y) + Okay: var = (1 &\n ~2) + Okay: var = (1 /\n -2) + Okay: var = (1 +\n -1 +\n -2) """ def is_binary_operator(token_type, text): # 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. return ((token_type == tokenize.OP or text in ['and', 'or']) and - text not in "()[]{},:.;@=%") + text not in "()[]{},:.;@=%~") line_break = False unary_context = True + # Previous non-newline token types and text + previous_token_type = None + previous_text = None for token_type, text, start, end, line in tokens: if token_type == tokenize.COMMENT: continue @@ -1048,10 +1103,14 @@ line_break = True else: if (is_binary_operator(token_type, text) and line_break and - not unary_context): + not unary_context and + not is_binary_operator(previous_token_type, + previous_text)): yield start, "W503 line break before binary operator" unary_context = text in '([{,;' line_break = False + previous_token_type = token_type + previous_text = text def comparison_to_singleton(logical_line, noqa): @@ -1084,8 +1143,8 @@ nonzero = ((singleton == 'True' and same) or (singleton == 'False' and not same)) msg += " or 'if %scond:'" % ('' if nonzero else 'not ') - yield (match.start(2), "%s comparison to %s should be %s" % - (code, singleton, msg), singleton, msg) + yield (match.start(2), ("%s comparison to %s should be %s" % + (code, singleton, msg)), singleton, msg) def comparison_negative(logical_line): @@ -1132,6 +1191,58 @@ yield match.start(), "E721 do not compare types, use 'isinstance()'" +def ambiguous_identifier(logical_line, tokens): + r"""Never use the characters 'l', 'O', or 'I' as variable names. + + In some fonts, these characters are indistinguishable from the numerals + one and zero. When tempted to use 'l', use 'L' instead. + + Okay: L = 0 + Okay: o = 123 + Okay: i = 42 + E741: l = 0 + E741: O = 123 + E741: I = 42 + + Variables can be bound in several other contexts, including class and + function definitions, 'global' and 'nonlocal' statements, exception + handlers, and 'with' statements. + + Okay: except AttributeError as o: + Okay: with lock as L: + E741: except AttributeError as O: + E741: with lock as l: + E741: global I + E741: nonlocal l + E742: class I(object): + E743: def l(x): + """ + 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 + # identifiers on the lhs of an assignment operator + if token_type == tokenize.OP and '=' in text: + if prev_text in idents_to_avoid: + ident = prev_text + pos = prev_start + # identifiers bound to a value with 'as', 'global', or 'nonlocal' + if prev_text in ('as', 'global', 'nonlocal'): + if text in idents_to_avoid: + ident = text + pos = start + if prev_text == 'class': + if text in idents_to_avoid: + yield start, "E742 ambiguous class definition '%s'", text + if prev_text == 'def': + if text in idents_to_avoid: + yield start, "E743 ambiguous function definition '%s'", text + if ident: + yield pos, "E741 ambiguous variable name '%s'", ident + prev_text = text + prev_start = start + + def python_3000_has_key(logical_line, noqa): r"""The {}.has_key() method is removed in Python 3: use the 'in' operator. @@ -1170,7 +1281,7 @@ def python_3000_backticks(logical_line): - r"""Backticks are removed in Python 3: use repr() instead. + r"""Use repr() instead of backticks in Python 3. Okay: val = repr(1 + 2) W604: val = `1 + 2` @@ -1201,7 +1312,7 @@ with open(filename, 'rb') as f: (coding, lines) = tokenize.detect_encoding(f.readline) f = TextIOWrapper(f, coding, line_buffering=True) - return [l.decode(coding) for l in lines] + f.readlines() + return [line.decode(coding) for line in lines] + f.readlines() except (LookupError, SyntaxError, UnicodeError): # Fall back if file encoding is improperly declared with open(filename, encoding='latin-1') as f: @@ -1209,7 +1320,9 @@ isidentifier = str.isidentifier def stdin_get_value(): + """Read the value from stdin.""" return TextIOWrapper(sys.stdin.buffer, errors='ignore').read() + noqa = re.compile(r'# no(?:qa|pep8)\b', re.I).search @@ -1313,8 +1426,18 @@ return any(fnmatch(filename, pattern) for pattern in patterns) +def update_counts(s, counts): + r"""Adds one to the counts of each appearance of characters in s, + for characters in counts""" + for char in s: + if char in counts: + counts[char] += 1 + + def _is_eol_token(token): return token[0] in NEWLINE or token[4][token[3][1]:].lstrip() == '\\\n' + + if COMMENT_WITH_NL: def _is_eol_token(token, _eol_token=_is_eol_token): return _eol_token(token) or (token[0] == tokenize.COMMENT and @@ -1364,6 +1487,8 @@ mod = inspect.getmodule(register_check) for (name, function) in inspect.getmembers(mod, inspect.isfunction): register_check(function) + + init_checks_registry() @@ -1412,6 +1537,7 @@ self.report = report or options.report self.report_error = self.report.error self.report_error_args = self.report.error_args + self.noqa = False # added for eric6 integration self.options = options @@ -1447,7 +1573,7 @@ return check(*arguments) def init_checker_state(self, name, argument_names): - """ Prepares a custom state for the specific checker plugin.""" + """ Prepare custom state for the specific checker plugin.""" if 'checker_state' in argument_names: self.checker_state = self._checker_states.setdefault(name, {}) @@ -1530,13 +1656,15 @@ if self.logical_line: self.previous_indent_level = self.indent_level self.previous_logical = self.logical_line + if not self.indent_level: + self.previous_unindented_logical_line = self.logical_line self.blank_lines = 0 self.tokens = [] def check_ast(self): """Build the file's AST and run all AST checks.""" try: - tree = compile(''.join(self.lines), '', 'exec', ast.PyCF_ONLY_AST) + tree = compile(''.join(self.lines), '', 'exec', PyCF_ONLY_AST) except (ValueError, SyntaxError, TypeError): return self.report_invalid_syntax() for name, cls, __ in self._ast_checks: @@ -1556,6 +1684,7 @@ for token in tokengen: if token[2][0] > self.total_lines: return + self.noqa = token[4] and noqa(token[4]) self.maybe_check_physical(token) yield token except (SyntaxError, tokenize.TokenError): @@ -1601,6 +1730,7 @@ self.indent_char = None self.indent_level = self.previous_indent_level = 0 self.previous_logical = '' + self.previous_unindented_logical_line = '' self.tokens = [] self.blank_lines = self.blank_before = 0 parens = 0 @@ -1866,7 +1996,8 @@ # # The default choice: ignore controversial checks # options.ignore = tuple(DEFAULT_IGNORE.split(',')) # else: - # Ignore all checks which are not explicitly selected or all if no +# # Ignore all checks which are not explicitly selected or all if no +# options.ignore = ('',) if options.select else tuple(options.ignore) # check is ignored or explicitly selected options.ignore = ('',) if options.select else tuple(options.ignore) @@ -1972,7 +2103,8 @@ return sorted(checks) -def get_parser(prog='pep8', version=__version__): +def get_parser(prog='pycodestyle', version=__version__): + """Create the parser for the program.""" parser = OptionParser(prog=prog, version=version, usage="%prog [options] input ...") parser.config_options = [ @@ -2039,7 +2171,7 @@ If a config file is specified on the command line with the "--config" option, then only it is used for configuration. - Otherwise, the user configuration (~/.config/pep8) and any local + Otherwise, the user configuration (~/.config/pycodestyle) and any local configurations in the current directory or above will be merged together (in that order) using the read method of ConfigParser. """ @@ -2087,13 +2219,12 @@ opt_type = option_list[normalized_opt] if opt_type in ('int', 'count'): value = config.getint(pep8_section, opt) - elif opt_type == 'string': + elif opt_type in ('store_true', 'store_false'): + value = config.getboolean(pep8_section, opt) + else: value = config.get(pep8_section, opt) if normalized_opt == 'exclude': value = normalize_paths(value, local_dir) - else: - assert opt_type in ('store_true', 'store_false') - value = config.getboolean(pep8_section, opt) setattr(new_options, normalized_opt, value) # Third, overwrite with the command-line options @@ -2107,7 +2238,7 @@ """Process options passed either via arglist or via command line args. Passing in the ``config_file`` parameter allows other tools, such as flake8 - to specify their own options to be processed in pep8. + to specify their own options to be processed in pycodestyle. """ if not parser: parser = get_parser() @@ -2179,14 +2310,14 @@ except AttributeError: pass # not supported on Windows - pep8style = StyleGuide(parse_argv=True) - options = pep8style.options + style_guide = StyleGuide(parse_argv=True) + options = style_guide.options if options.doctest or options.testsuite: from testsuite.support import run_tests - report = run_tests(pep8style) + report = run_tests(style_guide) else: - report = pep8style.check_files() + report = style_guide.check_files() if options.statistics: report.print_statistics()