Plugins/CheckerPlugins/CodeStyleChecker/pep8.py

changeset 5146
f96c32abd120
parent 4828
b313794f46a2
diff -r 95947ce71171 -r f96c32abd120 Plugins/CheckerPlugins/CodeStyleChecker/pep8.py
--- 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()

eric ide

mercurial