diff --git a/CHANGES.txt b/CHANGES.txt index 0957be83..ecfa609a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,52 @@ Changelog ========= -UNRELEASED ----------- +2.4.0 (2018-04-10) +------------------ New checks: -* Add W605 warning for invalid escape sequences in string literals +* Add W504 warning for checking that a break doesn't happen after a binary + operator. This check is ignored by default. PR #502. +* Add W605 warning for invalid escape sequences in string literals. PR #676. +* Add W606 warning for 'async' and 'await' reserved keywords being introduced + in Python 3.7. PR #684. +* Add E252 error for missing whitespace around equal sign in type annotated + function arguments with defaults values. PR #717. + +Changes: + +* An internal bisect search has replaced a linear search in order to improve + efficiency. PR #648. +* pycodestyle now uses PyPI trove classifiers in order to document supported + python versions on PyPI. PR #654. +* 'setup.cfg' '[wheel]' section has been renamed to '[bdist_wheel]', as + the former is legacy. PR #653. +* pycodestyle now handles very long lines much more efficiently for python + 3.2+. Fixes #643. PR #644. +* You can now write 'pycodestyle.StyleGuide(verbose=True)' instead of + 'pycodestyle.StyleGuide(verbose=True, paths=['-v'])' in order to achieve + verbosity. PR #663. +* The distribution of pycodestyle now includes the license text in order to + comply with open source licenses which require this. PR #694. +* 'maximum_line_length' now ignores shebang ('#!') lines. PR #736. +* Add configuration option for the allowed number of blank lines. It is + implemented as a top level dictionary which can be easily overwritten. Fixes + #732. PR #733. + +Bugs: + +* Prevent a 'DeprecationWarning', and a 'SyntaxError' in future python, caused + by an invalid escape sequence. PR #625. +* Correctly report E501 when the first line of a docstring is too long. + Resolves #622. PR #630. +* Support variable annotation when variable start by a keyword, such as class + variable type annotations in python 3.6. PR #640. +* pycodestyle internals have been changed in order to allow 'python3 -m + cProfile' to report correct metrics. PR #647. +* Fix a spelling mistake in the description of E722. PR #697. +* 'pycodestyle --diff' now does not break if your 'gitconfig' enables + 'mnemonicprefix'. PR #706. 2.3.1 (2017-01-31) ------------------ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index aad6ad67..9f558590 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -66,6 +66,44 @@ At this point you can create a pull request back to the official pycodestyles repository for review! For more information on how to make a pull request, GitHub has an excellent `guide`_. +The current tests are written in 2 styles: + +* standard xUnit based only on stdlib unittest + (can be executed with nose) +* functional test using a custom framework and executed by the + pycodestyle itself when installed in dev mode. + + +Running unittest +~~~~~~~~~~~~~~~~ + +While the tests are writted using stdlib `unittest` module, the existing +test include unit, integration and functional tests. + +There are a couple of ways to run the tests:: + + $ python setup.py test + $ # Use nose to run specific test + $ nosetests \ + > testsuite.test_blank_lines:TestBlankLinesDefault.test_initial_no_blank + $ # Use nose to run a subset and check coverage, and check the resulting + $ $ cover/pycodestyle_py.html in your browser + $ nosetests --with-coverage --cover-html -s testsuite.test_blank_lines + + +Running functional +~~~~~~~~~~~~~~~~~~ + +When installed in dev mode, pycodestyle will have the `--testsuite` +option which can be used to run the tests:: + + $ pip install -e . + $ # Run all tests. + $ pycodestyle --testsuite testsuite + $ # Run a subset of the tests. + $ pycodestyle --testsuite testsuite/E30.py + + .. _virtualenv: http://docs.python-guide.org/en/latest/dev/virtualenvs/ .. _guide: https://guides.github.com/activities/forking/ .. _tox: https://tox.readthedocs.io/en/latest/ diff --git a/LICENSE b/LICENSE index 302779d7..30ea0572 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Copyright © 2006-2009 Johann C. Rocholl Copyright © 2009-2014 Florent Xicluna -Copyright © 2014-2016 Ian Lee +Copyright © 2014-2018 Ian Lee Licensed under the terms of the Expat License diff --git a/docs/developer.rst b/docs/developer.rst index db36538d..8630edc2 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -14,7 +14,7 @@ conditions of the :ref:`Expat license `. Fork away! * `Source code `_ and `issue tracker `_ on GitHub. * `Continuous tests `_ against Python - 2.6 through 3.5 as well as the nightly Python build and PyPy, on `Travis-CI + 2.6 through 3.6 as well as the nightly Python build and PyPy, on `Travis CI platform `_. .. _available on GitHub: https://github.com/pycqa/pycodestyle @@ -104,7 +104,7 @@ Then be sure to pass the tests:: When contributing to pycodestyle, please observe our `Code of Conduct`_. -To run the tests, the core developer team and Travis-CI use tox:: +To run the tests, the core developer team and Travis CI use tox:: $ pip install -r dev-requirements.txt $ tox diff --git a/docs/intro.rst b/docs/intro.rst index dc4d689e..b594596f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -404,7 +404,9 @@ This is the current list of error and warning codes: +------------+----------------------------------------------------------------------+ | **W5** | *Line break warning* | +------------+----------------------------------------------------------------------+ -| W503 (*) | line break occurred before a binary operator | +| W503 (*)   | line break before binary operator                         | ++------------+----------------------------------------------------------------------+ +| W504 (*)   | line break after binary operator                         | +------------+----------------------------------------------------------------------+ | W505 (\*^) | doc line too long (82 > 79 characters) | +------------+----------------------------------------------------------------------+ @@ -421,13 +423,18 @@ This is the current list of error and warning codes: +------------+----------------------------------------------------------------------+ | W605 | invalid escape sequence '\x' | +------------+----------------------------------------------------------------------+ +| W606 | 'async' and 'await' are reserved keywords starting with Python 3.7 | ++------------+----------------------------------------------------------------------+ **(*)** In the default configuration, the checks **E121**, **E123**, **E126**, -**E133**, **E226**, **E241**, **E242**, **E704** and **W503** are ignored because -they are not rules unanimously accepted, and `PEP 8`_ does not enforce them. The -check **E133** is mutually exclusive with check **E123**. Use switch -``--hang-closing`` to report **E133** instead of **E123**. Use switch +**E133**, **E226**, **E241**, **E242**, **E704**, **W503** and **W504** are ignored +because they are not rules unanimously accepted, and `PEP 8`_ does not enforce them. +Please note that if the option **--ignore=errors** is used, +the default configuration will be overridden and ignore only the check(s) you skip. +The check **W503** is mutually exclusive with check **W504**. +The check **E133** is mutually exclusive with check **E123**. Use switch +``--hang-closing`` to report **E133** instead of **E123**. Use switch ``--max-doc-length=n`` to report **W505**. **(^)** These checks can be disabled at the line level using the ``# noqa`` diff --git a/pycodestyle.py b/pycodestyle.py index c4ce0b37..1784e171 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -78,10 +78,10 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation. except ImportError: from ConfigParser import RawConfigParser -__version__ = '2.3.1' +__version__ = '2.4.0' DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox' -DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503' +DEFAULT_IGNORE = 'E121,E123,E126,E226,E24,E704,W503,W504' try: if sys.platform == 'win32': USER_CONFIG = os.path.expanduser(r'~\.pycodestyle') @@ -96,6 +96,13 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation. PROJECT_CONFIG = ('setup.cfg', 'tox.ini') TESTSUITE_PATH = os.path.join(os.path.dirname(__file__), 'testsuite') MAX_LINE_LENGTH = 79 +# Number of blank lines between various code parts. +BLANK_LINES_CONFIG = { + # Top level class and function. + 'top_level': 2, + # Methods and nested class and function. + 'method': 1, +} MAX_DOC_LENGTH = 72 REPORT_FORMAT = { 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', @@ -104,7 +111,7 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation. PyCF_ONLY_AST = 1024 SINGLETONS = frozenset(['False', 'None', 'True']) -KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +KEYWORDS = frozenset(keyword.kwlist + ['print', 'async']) - SINGLETONS UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) ARITHMETIC_OP = frozenset(['**', '*', '/', '//', '+', '-']) WS_OPTIONAL_OPERATORS = ARITHMETIC_OP.union(['^', '&', '|', '<<', '>>', '%']) @@ -123,7 +130,7 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation. 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') @@ -263,7 +270,8 @@ def trailing_blank_lines(physical_line, lines, line_number, total_lines): @register_check -def maximum_line_length(physical_line, max_line_length, multiline, noqa): +def maximum_line_length(physical_line, max_line_length, multiline, + line_number, noqa): r"""Limit all lines to a maximum of 79 characters. There are still many devices around that are limited to 80 character @@ -278,6 +286,9 @@ def maximum_line_length(physical_line, max_line_length, multiline, noqa): line = physical_line.rstrip() length = len(line) if length > max_line_length and not noqa: + # Special case: ignore long shebang lines. + if line_number == 1 and line.startswith('#!'): + return # Special case for long URLs in multi-line docstrings or # comments, but still report the error when the 72 first chars # are whitespaces. @@ -334,39 +345,52 @@ def blank_lines(logical_line, blank_lines, indent_level, line_number, E304: @decorator\n\ndef a():\n pass E305: def a():\n pass\na() E306: def a():\n def b():\n pass\n def c():\n pass - """ # noqa - if line_number < 3 and not previous_logical: + """ # noqa + top_level_lines = BLANK_LINES_CONFIG['top_level'] + method_lines = BLANK_LINES_CONFIG['method'] + + if line_number < top_level_lines + 1 and not previous_logical: return # Don't expect blank lines before the first line if previous_logical.startswith('@'): if blank_lines: yield 0, "E304 blank lines found after function decorator" - elif blank_lines > 2 or (indent_level and blank_lines == 2): + elif (blank_lines > top_level_lines or + (indent_level and blank_lines == method_lines + 1) + ): yield 0, "E303 too many blank lines (%d)" % blank_lines elif STARTSWITH_TOP_LEVEL_REGEX.match(logical_line): if indent_level: - if not (blank_before or previous_indent_level < indent_level or - DOCSTRING_REGEX.match(previous_logical)): + if not (blank_before == method_lines or + previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical) + ): 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]: + for line in lines[line_number - top_level_lines::-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" + yield 0, "E306 expected %s blank line before a " \ + "nested definition, found 0" % (method_lines,) 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 + yield 0, "E301 expected %s blank line, found 0" % ( + method_lines,) + elif blank_before != top_level_lines: + yield 0, "E302 expected %s blank lines, found %d" % ( + top_level_lines, blank_before) + elif (logical_line and + not indent_level and + blank_before != top_level_lines and + previous_unindented_logical_line.startswith(('def ', 'class ')) + ): + yield 0, "E305 expected %s blank lines after " \ + "class or function definition, found %d" % ( + top_level_lines, blank_before) @register_check @@ -879,7 +903,8 @@ def whitespace_around_named_parameter_equals(logical_line, tokens): r"""Don't use spaces around the '=' sign in function arguments. Don't use spaces around the '=' sign when used to indicate a - keyword argument or a default parameter value. + keyword argument or a default parameter value, except when + using a type annotation. Okay: def complex(real, imag=0.0): Okay: return magic(r=real, i=imag) @@ -892,13 +917,18 @@ def whitespace_around_named_parameter_equals(logical_line, tokens): E251: def complex(real, imag = 0.0): E251: return magic(r = real, i = imag) + E252: def complex(real, image: float=0.0): """ parens = 0 no_space = False + require_space = False prev_end = None annotated_func_arg = False in_def = bool(STARTSWITH_DEF_REGEX.match(logical_line)) + message = "E251 unexpected spaces around keyword / parameter equals" + missing_message = "E252 missing whitespace around parameter equals" + for token_type, text, start, end, line in tokens: if token_type == tokenize.NL: continue @@ -906,6 +936,10 @@ def whitespace_around_named_parameter_equals(logical_line, tokens): no_space = False if start != prev_end: yield (prev_end, message) + if require_space: + require_space = False + if start == prev_end: + yield (prev_end, missing_message) if token_type == tokenize.OP: if text in '([': parens += 1 @@ -913,12 +947,17 @@ def whitespace_around_named_parameter_equals(logical_line, tokens): parens -= 1 elif in_def and text == ':' and parens == 1: annotated_func_arg = True - elif parens and text == ',' and parens == 1: + elif parens == 1 and text == ',': annotated_func_arg = False - elif parens and text == '=' and not annotated_func_arg: - no_space = True - if start != prev_end: - yield (prev_end, message) + elif parens and text == '=': + if annotated_func_arg and parens == 1: + require_space = True + if start == prev_end: + yield (prev_end, missing_message) + else: + no_space = True + if start != prev_end: + yield (prev_end, message) if not parens: annotated_func_arg = False @@ -1148,34 +1187,26 @@ def explicit_line_join(logical_line, tokens): parens -= 1 -@register_check -def break_around_binary_operator(logical_line, tokens): - r""" - Avoid breaks before binary operators. +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 "()[]{},:.;@=%~" + # 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 ((is_op_token or is_conjunction) and not_a_symbol) - The preferred place to break around a binary operator is after the - operator, not before it. - W503: (width == 0\n + height == 0) - W503: (width == 0\n and height == 0) +def _break_around_binary_operators(tokens): + """Private function to reduce duplication. - Okay: (width == 0 +\n height == 0) - Okay: foo(\n -x) - Okay: foo(x\n []) - 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) + This factors out the shared details between + :func:`break_before_binary_operator` and + :func:`break_after_binary_operator`. """ - 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 "()[]{},:.;@=%~") - line_break = False unary_context = True # Previous non-newline token types and text @@ -1187,17 +1218,78 @@ def is_binary_operator(token_type, text): if ('\n' in text or '\r' in text) and token_type != tokenize.STRING: line_break = True else: - if (is_binary_operator(token_type, text) and line_break and - not unary_context and - not is_binary_operator(previous_token_type, - previous_text)): - yield start, "W503 line break before binary operator" + yield (token_type, text, previous_token_type, previous_text, + line_break, unary_context, start) unary_context = text in '([{,;' line_break = False previous_token_type = token_type previous_text = text +@register_check +def break_before_binary_operator(logical_line, tokens): + r""" + Avoid breaks before binary operators. + + The preferred place to break around a binary operator is after the + operator, not before it. + + W503: (width == 0\n + height == 0) + W503: (width == 0\n and height == 0) + W503: var = (1\n & ~2) + W503: var = (1\n / -2) + W503: var = (1\n + -1\n + -2) + + Okay: foo(\n -x) + Okay: foo(x\n []) + Okay: x = '''\n''' + '' + Okay: foo(x,\n -y) + Okay: foo(x, # comment\n -y) + """ + for context in _break_around_binary_operators(tokens): + (token_type, text, previous_token_type, previous_text, + line_break, unary_context, start) = context + if (_is_binary_operator(token_type, text) and line_break and + not unary_context and + not _is_binary_operator(previous_token_type, + previous_text)): + yield start, "W503 line break before binary operator" + + +@register_check +def break_after_binary_operator(logical_line, tokens): + r""" + Avoid breaks after binary operators. + + The preferred place to break around a binary operator is before the + operator, not after it. + + W504: (width == 0 +\n height == 0) + W504: (width == 0 and\n height == 0) + W504: var = (1 &\n ~2) + + Okay: foo(\n -x) + Okay: foo(x\n []) + Okay: x = '''\n''' + '' + Okay: x = '' + '''\n''' + Okay: foo(x,\n -y) + Okay: foo(x, # comment\n -y) + + The following should be W504 but unary_context is tricky with these + Okay: var = (1 /\n -2) + Okay: var = (1 +\n -1 +\n -2) + """ + for context in _break_around_binary_operators(tokens): + (token_type, text, previous_token_type, previous_text, + line_break, unary_context, start) = context + if (_is_binary_operator(previous_token_type, previous_text) and + line_break and + not unary_context and + not _is_binary_operator(token_type, text)): + error_pos = (start[0] - 1, start[1]) + yield error_pos, "W504 line break after binary operator" + + @register_check def comparison_to_singleton(logical_line, noqa): r"""Comparison to singletons should use "is" or "is not". @@ -1447,13 +1539,68 @@ def python_3000_invalid_escape_sequence(logical_line, tokens): pos += 1 if string[pos] not in valid: yield ( - pos, + line.lstrip().find(text), "W605 invalid escape sequence '\\%s'" % string[pos], ) pos = string.find('\\', pos + 1) +@register_check +def python_3000_async_await_keywords(logical_line, tokens): + """'async' and 'await' are reserved keywords starting with Python 3.7 + + W606: async = 42 + W606: await = 42 + Okay: async def read_data(db):\n data = await db.fetch('SELECT ...') + """ + # The Python tokenize library before Python 3.5 recognizes async/await as a + # NAME token. Therefore, use a state machine to look for the possible + # async/await constructs as defined by the Python grammar: + # https://docs.python.org/3/reference/grammar.html + + state = None + for token_type, text, start, end, line in tokens: + error = False + + if state is None: + if token_type == tokenize.NAME: + if text == 'async': + state = ('async_stmt', start) + elif text == 'await': + state = ('await', start) + elif state[0] == 'async_stmt': + if token_type == tokenize.NAME and text in ('def', 'with', 'for'): + # One of funcdef, with_stmt, or for_stmt. Return to looking + # for async/await names. + state = None + else: + error = True + elif state[0] == 'await': + if token_type in (tokenize.NAME, tokenize.NUMBER, tokenize.STRING): + # An await expression. Return to looking for async/await names. + state = None + else: + error = True + + if error: + yield ( + state[1], + "W606 'async' and 'await' are reserved keywords starting with " + "Python 3.7", + ) + state = None + + # Last token + if state is not None: + yield ( + state[1], + "W606 'async' and 'await' are reserved keywords starting with " + "Python 3.7", + ) + + +############################################################################## @register_check def maximum_doc_length(logical_line, max_doc_length, noqa, tokens): r"""Limit all doc lines to a maximum of 72 characters. @@ -1605,12 +1752,14 @@ def parse_udiff(diff, patterns=None, parent='.'): rv[path].update(range(row, row + nrows)) elif line[:3] == '+++': path = line[4:].split('\t', 1)[0] - if path[:2] == 'b/': + # Git diff will use (i)ndex, (w)ork tree, (c)ommit and (o)bject + # instead of a/b/c/d as prefixes for patches + if path[:2] in ('b/', 'w/', 'i/'): path = path[2:] rv[path] = set() - return dict([(os.path.join(parent, path), rows) - for (path, rows) in rv.items() - if rows and filename_match(path, patterns)]) + return dict([(os.path.join(parent, filepath), rows) + for (filepath, rows) in rv.items() + if rows and filename_match(filepath, patterns)]) def normalize_paths(value, parent=os.curdir): diff --git a/setup.cfg b/setup.cfg index c76e3e15..73ae4e79 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,6 @@ license_file = LICENSE [pycodestyle] select = -ignore = E226,E24 +ignore = E226,E24,W504 max_line_length = 79 max_doc_length = 72 diff --git a/testsuite/E12.py b/testsuite/E12.py index a995c955..acdd81f6 100644 --- a/testsuite/E12.py +++ b/testsuite/E12.py @@ -20,9 +20,9 @@ #: E124 a = (123, ) -#: E129 -if (row < 0 or self.moduleCount <= row or - col < 0 or self.moduleCount <= col): +#: E129 W503 +if (row < 0 or self.moduleCount <= row + or col < 0 or self.moduleCount <= col): raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) #: E126 print "E126", ( @@ -195,9 +195,9 @@ def qualify_by_address( self, cr, uid, ids, context=None, params_to_check=frozenset(QUALIF_BY_ADDRESS_PARAM)): """ This gets called by the web server """ -#: E129 -if (a == 2 or - b == "abc def ghi" +#: E129 W503 +if (a == 2 + or b == "abc def ghi" "jkl mno"): return True #: @@ -225,22 +225,21 @@ def qualify_by_address( eat_a_dict_a_day({ "foo": "bar", }) -#: E126 +#: E126 W503 if ( x == ( 3 - ) or - y == 4): + ) + or y == 4): pass -#: E126 +#: E126 W503 W503 if ( x == ( 3 - ) or - x == ( - 3 - ) or - y == 4): + ) + or x == ( + 3) + or y == 4): pass #: E131 troublesome_hash = { diff --git a/testsuite/E12not.py b/testsuite/E12not.py index 65281075..ebaa078f 100644 --- a/testsuite/E12not.py +++ b/testsuite/E12not.py @@ -1,8 +1,7 @@ if ( x == ( 3 - ) or - y == 4): + ) or y == 4): pass y = x == 2 \ @@ -17,15 +16,14 @@ or y > 1 \ or x == 3: pass - - -if (foo == bar and - baz == frop): +#: W503 +if (foo == bar + and baz == frop): pass - +#: W503 if ( - foo == bar and - baz == frop + foo == bar + and baz == frop ): pass @@ -109,7 +107,7 @@ 'BBB' \ 'iii' \ 'CCC' - +#: W504 W504 abricot = (3 + 4 + 5 + 6) @@ -138,8 +136,7 @@ def long_function_name( var_one, var_two, var_three, var_four): print(var_one) - - +#: W504 if ((row < 0 or self.moduleCount <= row or col < 0 or self.moduleCount <= col)): raise Exception("%s,%s - %s" % (row, col, self.moduleCount)) @@ -184,23 +181,23 @@ def long_function_name( "to match that of the opening " "bracket's line" ) -# +#: W504 # you want vertical alignment, so use a parens if ((foo.bar("baz") and foo.bar("frop") )): print "yes" - +#: W504 # also ok, but starting to look like LISP if ((foo.bar("baz") and foo.bar("frop"))): print "yes" - +#: W504 if (a == 2 or b == "abc def ghi" "jkl mno"): return True - +#: W504 if (a == 2 or b == """abc def ghi jkl mno"""): @@ -224,22 +221,19 @@ def long_function_name( print('%-7d %s per second (%d total)' % ( options.counters[key] / elapsed, key, options.counters[key])) - - +#: W504 if os.path.exists(os.path.join(path, PEP8_BIN)): cmd = ([os.path.join(path, PEP8_BIN)] + self._pep8_options(targetfile)) - - +#: W504 fixed = (re.sub(r'\t+', ' ', target[c::-1], 1)[::-1] + target[c + 1:]) - +#: W504 fixed = ( re.sub(r'\t+', ' ', target[c::-1], 1)[::-1] + target[c + 1:] ) - - +#: W504 if foo is None and bar is "frop" and \ blah == 'yeah': blah = 'yeahnah' diff --git a/testsuite/E25.py b/testsuite/E25.py index dde95b8e..88981576 100644 --- a/testsuite/E25.py +++ b/testsuite/E25.py @@ -39,6 +39,12 @@ def munge(input: AnyStr, sep: AnyStr = None, limit=1000, async def add(a: int = 0, b: int = 0) -> int: return a + b # Previously E251 four times -#: E272:1:6 +#: E271:1:6 async def add(a: int = 0, b: int = 0) -> int: return a + b +#: E252:1:15 E252:1:16 E252:1:27 E252:1:36 +def add(a: int=0, b: int =0, c: int= 0) -> int: + return a + b + c +#: Okay +def add(a: int = _default(name='f')): + return a diff --git a/testsuite/E30.py b/testsuite/E30.py index bd74b803..ad5518bb 100644 --- a/testsuite/E30.py +++ b/testsuite/E30.py @@ -157,7 +157,7 @@ def main(): if __name__ == '__main__': main() # Previously just E272:1:6 E272:4:6 -#: E302:4:1 E272:1:6 E272:4:6 +#: E302:4:1 E271:1:6 E271:4:6 async def x(): pass diff --git a/testsuite/E50.py b/testsuite/E50.py index 2845faa7..bcf3bdce 100644 --- a/testsuite/E50.py +++ b/testsuite/E50.py @@ -122,3 +122,7 @@ def foo(): #: E501 W505 # This # almost_empty_line + +# +#: Okay +#!/reallylongpath/toexecutable --maybe --with --some ARGUMENTS TO DO WITH WHAT EXECUTABLE TO RUN diff --git a/testsuite/E70.py b/testsuite/E70.py index caafe455..7c01edbb 100644 --- a/testsuite/E70.py +++ b/testsuite/E70.py @@ -14,7 +14,7 @@ def f(x): return 2 #: E704:1:1 async def f(x): return 2 -#: E704:1:1 E272:1:6 +#: E704:1:1 E271:1:6 async def f(x): return 2 #: E704:1:1 E226:1:19 def f(x): return 2*x diff --git a/testsuite/W19.py b/testsuite/W19.py index afdfb767..ed69e2b5 100644 --- a/testsuite/W19.py +++ b/testsuite/W19.py @@ -7,7 +7,7 @@ #: W191 y = x == 2 \ or x == 3 -#: E101 W191 +#: E101 W191 W504 if ( x == ( 3 @@ -26,11 +26,11 @@ pass #: -#: E101 W191 +#: E101 W191 W504 if (foo == bar and baz == frop): pass -#: E101 W191 +#: E101 W191 W504 if ( foo == bar and baz == frop @@ -52,7 +52,7 @@ def long_function_name( var_one, var_two, var_three, var_four): print(var_one) -#: E101 W191 +#: 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)) @@ -65,23 +65,23 @@ def long_function_name( "bracket's line" ) # -#: E101 W191 +#: E101 W191 W504 # you want vertical alignment, so use a parens if ((foo.bar("baz") and foo.bar("frop") )): print "yes" -#: E101 W191 +#: E101 W191 W504 # also ok, but starting to look like LISP if ((foo.bar("baz") and foo.bar("frop"))): print "yes" -#: E101 W191 +#: E101 W191 W504 if (a == 2 or b == "abc def ghi" "jkl mno"): return True -#: E101 W191 +#: E101 W191 W504 if (a == 2 or b == """abc def ghi jkl mno"""): @@ -93,7 +93,7 @@ def long_function_name( # -#: E101 W191 W191 +#: 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)) diff --git a/testsuite/W60.py b/testsuite/W60.py index cbe267d5..030bec59 100644 --- a/testsuite/W60.py +++ b/testsuite/W60.py @@ -29,3 +29,48 @@ \\.png$ ''' s = '\\' +#: W606 +async = 42 +#: W606 +await = 42 +#: W606 +def async(): + pass +#: W606 +def await(): + pass +#: W606 +class async: + pass +#: W606 +class await: + pass +#: Okay +async def read_data(db): + data = await db.fetch('SELECT ...') +#: Okay +if await fut: + pass +if (await fut): + pass +if await fut + 1: + pass +if (await fut) + 1: + pass +pair = await fut, 'spam' +pair = (await fut), 'spam' +with await fut, open(): + pass +with (await fut), open(): + pass +await foo()['spam'].baz()() +return await coro() +return (await coro()) +res = await coro() ** 2 +res = (await coro()) ** 2 +func(a1=await coro(), a2=0) +func(a1=(await coro()), a2=0) +await foo() + await bar() +(await foo()) + (await bar()) +-await foo() +-(await foo()) diff --git a/testsuite/support.py b/testsuite/support.py index d2e1ba7d..bcca4e49 100644 --- a/testsuite/support.py +++ b/testsuite/support.py @@ -83,6 +83,26 @@ def print_results(self): print("Test failed." if self.total_errors else "Test passed.") +class InMemoryReport(BaseReport): + """ + Collect the results in memory, without printing anything. + """ + + def __init__(self, options): + super(InMemoryReport, self).__init__(options) + self.in_memory_errors = [] + + def error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = text[:4] + self.in_memory_errors.append('%s:%s:%s' % ( + code, line_number, offset + 1)) + return super(InMemoryReport, self).error( + line_number, offset, text, check) + + def selftest(options): """ Test all check functions with test cases in docstrings. diff --git a/testsuite/test_all.py b/testsuite/test_all.py index 08f9ea91..0e4bc7d1 100644 --- a/testsuite/test_all.py +++ b/testsuite/test_all.py @@ -43,16 +43,24 @@ def test_own_dog_food(self): os.path.join(ROOT_DIR, 'setup.py')] report = self._style.init_report(pycodestyle.StandardReport) report = self._style.check_files(files) - self.assertFalse(report.total_errors, + self.assertEqual(list(report.messages.keys()), ['W504'], msg='Failures: %s' % report.messages) def suite(): - from testsuite import test_api, test_parser, test_shell, test_util + from testsuite import ( + test_api, + test_blank_lines, + test_parser, + test_shell, + test_util, + ) suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(PycodestyleTestCase)) suite.addTest(unittest.makeSuite(test_api.APITestCase)) + suite.addTest(unittest.makeSuite(test_blank_lines.TestBlankLinesDefault)) + suite.addTest(unittest.makeSuite(test_blank_lines.TestBlankLinesTwisted)) suite.addTest(unittest.makeSuite(test_parser.ParserTestCase)) suite.addTest(unittest.makeSuite(test_shell.ShellTestCase)) suite.addTest(unittest.makeSuite(test_util.UtilTestCase)) diff --git a/testsuite/test_api.py b/testsuite/test_api.py index 8494719f..83b8d73d 100644 --- a/testsuite/test_api.py +++ b/testsuite/test_api.py @@ -166,7 +166,7 @@ def test_styleguide_options(self): self.assertEqual(pep8style.options.filename, ['*.py']) self.assertEqual(pep8style.options.format, 'default') self.assertEqual(pep8style.options.select, ()) - self.assertEqual(pep8style.options.ignore, ('E226', 'E24')) + self.assertEqual(pep8style.options.ignore, ('E226', 'E24', 'W504')) self.assertEqual(pep8style.options.max_line_length, 79) def test_styleguide_ignore_code(self): @@ -182,7 +182,7 @@ def parse_argv(argstring): self.assertEqual(options.select, ()) self.assertEqual( options.ignore, - ('E121', 'E123', 'E126', 'E226', 'E24', 'E704', 'W503') + ('E121', 'E123', 'E126', 'E226', 'E24', 'E704', 'W503', 'W504') ) options = parse_argv('--doctest').options diff --git a/testsuite/test_blank_lines.py b/testsuite/test_blank_lines.py new file mode 100644 index 00000000..870403a4 --- /dev/null +++ b/testsuite/test_blank_lines.py @@ -0,0 +1,552 @@ +""" +Tests for the blank_lines checker. + +It uses dedicated assertions which work with TestReport. +""" +import unittest + +import pycodestyle +from testsuite.support import InMemoryReport + + +class BlankLinesTestCase(unittest.TestCase): + """ + Common code for running blank_lines tests. + """ + + def check(self, content): + """ + Run checks on `content` and return the the list of errors. + """ + sut = pycodestyle.StyleGuide() + reporter = sut.init_report(InMemoryReport) + sut.input_file( + filename='in-memory-test-file.py', + lines=content.splitlines(True), + ) + return reporter.in_memory_errors + + def assertNoErrors(self, actual): + """ + Check that the actual result from the checker has no errors. + """ + self.assertEqual([], actual) + + +class TestBlankLinesDefault(BlankLinesTestCase): + """ + Tests for default blank with 2 blank lines for top level and 1 blank line + for methods. + """ + + def test_initial_no_blank(self): + """ + It will accept no blank lines at the start of the file. + """ + result = self.check("""def some_function(): + pass +""") + + self.assertNoErrors(result) + + def test_initial_lines_one_blank(self): + """ + It will accept 1 blank lines before the first line of actual code, + even if in other places it asks for 2 + """ + result = self.check(""" +def some_function(): + pass +""") + + self.assertNoErrors(result) + + def test_initial_lines_two_blanks(self): + """ + It will accept 2 blank lines before the first line of actual code, + as normal. + """ + result = self.check(""" + +def some_function(): + pass +""") + + self.assertNoErrors(result) + + def test_method_less_blank_lines(self): + """ + It will trigger an error when less than 1 blank lin is found before + method definitions. + """ + result = self.check("""# First comment line. +class X: + + def a(): + pass + def b(): + pass +""") + self.assertEqual([ + 'E301:6:5', # b() call + ], result) + + def test_method_less_blank_lines_comment(self): + """ + It will trigger an error when less than 1 blank lin is found before + method definition, ignoring comments. + """ + result = self.check("""# First comment line. +class X: + + def a(): + pass + # A comment will not make it better. + def b(): + pass +""") + self.assertEqual([ + 'E301:7:5', # b() call + ], result) + + def test_top_level_fewer_blank_lines(self): + """ + It will trigger an error when less 2 blank lines are found before top + level definitions. + """ + result = self.check("""# First comment line. +# Second line of comment. + +def some_function(): + pass + +async def another_function(): + pass + + +def this_one_is_good(): + pass + +class SomeCloseClass(object): + pass + + +async def this_async_is_good(): + pass + + +class AFarEnoughClass(object): + pass +""") + self.assertEqual([ + 'E302:4:1', # some_function + 'E302:7:1', # another_function + 'E302:14:1', # SomeCloseClass + ], result) + + def test_top_level_more_blank_lines(self): + """ + It will trigger an error when more 2 blank lines are found before top + level definitions. + """ + result = self.check("""# First comment line. +# Second line of comment. + + + +def some_function(): + pass + + +def this_one_is_good(): + pass + + + +class SomeFarClass(object): + pass + + +class AFarEnoughClass(object): + pass +""") + self.assertEqual([ + 'E303:6:1', # some_function + 'E303:15:1', # SomeFarClass + ], result) + + def test_method_more_blank_lines(self): + """ + It will trigger an error when more than 1 blank line is found before + method definition + """ + result = self.check("""# First comment line. + + +class SomeCloseClass(object): + + + def oneMethod(self): + pass + + + def anotherMethod(self): + pass + + def methodOK(self): + pass + + + + def veryFar(self): + pass +""") + self.assertEqual([ + 'E303:7:5', # oneMethod + 'E303:11:5', # anotherMethod + 'E303:19:5', # veryFar + ], result) + + def test_initial_lines_more_blank(self): + """ + It will trigger an error for more than 2 blank lines before the first + line of actual code. + """ + result = self.check(""" + + +def some_function(): + pass +""") + self.assertEqual(['E303:4:1'], result) + + def test_blank_line_between_decorator(self): + """ + It will trigger an error when the decorator is followed by a blank + line. + """ + result = self.check("""# First line. + + +@some_decorator + +def some_function(): + pass + + +class SomeClass(object): + + @method_decorator + + def some_method(self): + pass +""") + self.assertEqual(['E304:6:1', 'E304:14:5'], result) + + def test_blank_line_decorator(self): + """ + It will accept the decorators which are adjacent to the function and + method definition. + """ + result = self.check("""# First line. + + +@another_decorator +@some_decorator +def some_function(): + pass + + +class SomeClass(object): + + @method_decorator + def some_method(self): + pass +""") + self.assertNoErrors(result) + + def test_top_level_fewer_follow_lines(self): + """ + It will trigger an error when less than 2 blank lines are + found between a top level definitions and other top level code. + """ + result = self.check(""" +def a(): + print('Something') + +a() +""") + self.assertEqual([ + 'E305:5:1', # a call + ], result) + + def test_top_level_fewer_follow_lines_comments(self): + """ + It will trigger an error when less than 2 blank lines are + found between a top level definitions and other top level code, + even if we have comments before + """ + result = self.check(""" +def a(): + print('Something') + + # comment + + # another comment + +# With comment still needs 2 spaces before, +# as comments are ignored. +a() +""") + self.assertEqual([ + 'E305:11:1', # a call + ], result) + + def test_top_level_good_follow_lines(self): + """ + It not trigger an error when 2 blank lines are + found between a top level definitions and other top level code. + """ + result = self.check(""" +def a(): + print('Something') + + # Some comments in other parts. + + # More comments. + + +# With the right spaces, +# It will work, even when we have comments. +a() +""") + self.assertNoErrors(result) + + def test_method_fewer_follow_lines(self): + """ + It will trigger an error when less than 1 blank line is + found between a method and previous definitions. + """ + result = self.check(""" +def a(): + x = 1 + def b(): + pass +""") + self.assertEqual([ + 'E306:4:5', # b() call + ], result) + + def test_method_nested_fewer_follow_lines(self): + """ + It will trigger an error when less than 1 blank line is + found between a method and previous definitions, even when nested. + """ + result = self.check(""" +def a(): + x = 2 + + def b(): + x = 1 + def c(): + pass +""") + self.assertEqual([ + 'E306:7:9', # c() call + ], result) + + def test_method_nested_less_class(self): + """ + It will trigger an error when less than 1 blank line is found + between a method and previous definitions, even when used to + define a class. + """ + result = self.check(""" +def a(): + x = 1 + class C: + pass +""") + self.assertEqual([ + 'E306:4:5', # class C definition. + ], result) + + def test_method_nested_ok(self): + """ + Will not trigger an error when 1 blank line is found + found between a method and previous definitions, even when nested. + """ + result = self.check(""" +def a(): + x = 2 + + def b(): + x = 1 + + def c(): + pass + + class C: + pass +""") + self.assertNoErrors(result) + + +class TestBlankLinesTwisted(BlankLinesTestCase): + """ + Tests for blank_lines with 3 blank lines for top level and 2 blank line + for methods as used by the Twisted coding style. + """ + + def setUp(self): + self._original_lines_config = pycodestyle.BLANK_LINES_CONFIG.copy() + pycodestyle.BLANK_LINES_CONFIG['top_level'] = 3 + pycodestyle.BLANK_LINES_CONFIG['method'] = 2 + + def tearDown(self): + pycodestyle.BLANK_LINES_CONFIG = self._original_lines_config + + def test_initial_lines_one_blanks(self): + """ + It will accept less than 3 blank lines before the first line of actual + code. + """ + result = self.check(""" + + +def some_function(): + pass +""") + + self.assertNoErrors(result) + + def test_initial_lines_tree_blanks(self): + """ + It will accept 3 blank lines before the first line of actual code, + as normal. + """ + result = self.check(""" + + +def some_function(): + pass +""") + + self.assertNoErrors(result) + + def test_top_level_fewer_blank_lines(self): + """ + It will trigger an error when less 2 blank lines are found before top + level definitions. + """ + result = self.check("""# First comment line. +# Second line of comment. + + +def some_function(): + pass + + +async def another_function(): + pass + + + +def this_one_is_good(): + pass + +class SomeCloseClass(object): + pass + + + +async def this_async_is_good(): + pass + + + +class AFarEnoughClass(object): + pass +""") + self.assertEqual([ + 'E302:5:1', # some_function + 'E302:9:1', # another_function + 'E302:17:1', # SomeCloseClass + ], result) + + def test_top_level_more_blank_lines(self): + """ + It will trigger an error when more 2 blank lines are found before top + level definitions. + """ + result = self.check("""# First comment line. +# Second line of comment. + + + + +def some_function(): + pass + + + +def this_one_is_good(): + pass + + + + +class SomeVeryFarClass(object): + pass + + + +class AFarEnoughClass(object): + pass +""") + self.assertEqual([ + 'E303:7:1', # some_function + 'E303:18:1', # SomeVeryFarClass + ], result) + + def test_the_right_blanks(self): + """ + It will accept 3 blank for top level and 2 for nested. + """ + result = self.check(""" + + +def some_function(): + pass + + + +# With comments. +some_other = code_here + + + +class SomeClass: + ''' + Docstring here. + ''' + + def some_method(): + pass + + + def another_method(): + pass + + + # More methods. + def another_method_with_comment(): + pass + + + @decorator + def another_method_with_comment(): + pass +""") + + self.assertNoErrors(result) diff --git a/testsuite/test_shell.py b/testsuite/test_shell.py index a80c8757..7ada1a41 100644 --- a/testsuite/test_shell.py +++ b/testsuite/test_shell.py @@ -191,3 +191,13 @@ def test_check_diff(self): self.assertFalse(errcode) self.assertFalse(stdout) self.assertFalse(stderr) + + for index, diff_line in enumerate(diff_lines, 0): + diff_line = diff_line.replace('a/', 'i/') + diff_lines[index] = diff_line.replace('b/', 'w/') + + self.stdin = '\n'.join(diff_lines) + stdout, stderr, errcode = self.pycodestyle('--diff') + self.assertFalse(errcode) + self.assertFalse(stdout) + self.assertFalse(stderr) diff --git a/tox.ini b/tox.ini index baf5cf2d..98df04f3 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,8 @@ [tox] envlist = py26, py27, py32, py33, py34, py35, py36, pypy, pypy3, jython -skip_missing_interpreters=True +skipsdist = True +skip_missing_interpreters = True [testenv] commands =