From 1711fb429ae4815faa55bc9ccceca59f27f88706 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 29 Sep 2017 16:34:44 -0700 Subject: [PATCH 01/22] Add W606 warning for async and await keywords in Python 3.7 From https://docs.python.org/3/whatsnew/3.6.html#new-keywords > async and await are not recommended to be used as variable, class, > function or module names. Introduced by PEP 492 in Python 3.5, they > will become proper keywords in Python 3.7. Starting in Python 3.6, the > use of async or await as names will generate a DeprecationWarning. By adding a warning to pycodestyle.py these future warnings and syntax errors can be caught during static code analysis. The await expression tests were taken from PEP-492. https://www.python.org/dev/peps/pep-0492/#id58 --- CHANGES.txt | 2 ++ docs/intro.rst | 2 ++ pycodestyle.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ testsuite/W60.py | 45 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0957be83..8fedbaa3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,8 @@ UNRELEASED New checks: * Add W605 warning for invalid escape sequences in string literals +* Add W606 warning for 'async' and 'await' reserved keywords being introduced + in Python 3.7 2.3.1 (2017-01-31) ------------------ diff --git a/docs/intro.rst b/docs/intro.rst index 3035a2f2..4b064d05 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -415,6 +415,8 @@ 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**, diff --git a/pycodestyle.py b/pycodestyle.py index d31ac9e8..ad67fb08 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1439,6 +1439,60 @@ def python_3000_invalid_escape_sequence(logical_line, tokens): 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", + ) + + ############################################################################## # Helper functions ############################################################################## 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()) From b556d30f6426c854140fb13521159071ee529056 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 11 Dec 2017 12:13:37 +0200 Subject: [PATCH 02/22] Add Python 3.6 Plus Travis CI typo --- docs/developer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From d9efb8f46220dc24f74cb8e0f963786d47deecdc Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Sat, 16 Dec 2017 13:29:23 -0500 Subject: [PATCH 03/22] Add E252 on missing whitespace for annotated parameter defaults --- pycodestyle.py | 25 ++++++++++++++++++++----- testsuite/E25.py | 3 +++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index 1b066911..3fec26c0 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -869,7 +869,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) @@ -882,13 +883,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 @@ -896,6 +902,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 @@ -905,10 +915,15 @@ def whitespace_around_named_parameter_equals(logical_line, tokens): annotated_func_arg = True elif parens and text == ',' and parens == 1: 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 not annotated_func_arg: + no_space = True + if start != prev_end: + yield (prev_end, message) + else: + require_space = True + if start == prev_end: + yield (prev_end, missing_message) if not parens: annotated_func_arg = False diff --git a/testsuite/E25.py b/testsuite/E25.py index dde95b8e..71d3f800 100644 --- a/testsuite/E25.py +++ b/testsuite/E25.py @@ -42,3 +42,6 @@ async def add(a: int = 0, b: int = 0) -> int: #: E272: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 From 766c78a01d9ef8908f8254d9e8a19cac28c8d048 Mon Sep 17 00:00:00 2001 From: Michal Kolodziejski Date: Wed, 6 Dec 2017 21:34:46 +0100 Subject: [PATCH 04/22] Fix handling of diffs with mnemonic prefixes --- pycodestyle.py | 4 +++- testsuite/test_shell.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pycodestyle.py b/pycodestyle.py index 1b066911..eb9ff634 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1538,7 +1538,9 @@ 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) 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) From 8f3aebdd2bbe08111e7bc76f2722bab965ab571c Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 30 Apr 2016 07:52:29 -0500 Subject: [PATCH 05/22] Add W504 for line breaks before binary operators This flips the W503 rule to enforce line breaks before binary operators. Related #498 --- pycodestyle.py | 109 ++++++++++++++++++++++++++++++++------------ testsuite/E12.py | 29 ++++++------ testsuite/E12not.py | 11 ++--- 3 files changed, 100 insertions(+), 49 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index eb9ff634..ad173829 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -80,7 +80,7 @@ def lru_cache(maxsize=128): # noqa as it's a fake implementation. __version__ = '2.3.1' 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') @@ -1135,8 +1135,47 @@ def explicit_line_join(logical_line, tokens): parens -= 1 +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) + + +def _break_around_binary_operators(tokens): + """Private function to reduce duplication. + + This factors out the shared details between + :func:`break_before_binary_operator` and + :func:`break_after_binary_operator`. + """ + 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 + if ('\n' in text or '\r' in text) and token_type != tokenize.STRING: + line_break = True + else: + 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_around_binary_operator(logical_line, tokens): +def break_before_binary_operator(logical_line, tokens): r""" Avoid breaks before binary operators. @@ -1156,33 +1195,47 @@ def break_around_binary_operator(logical_line, tokens): 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 "()[]{},:.;@=%~") + 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" - 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 - 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" - unary_context = text in '([{,;' - line_break = False - previous_token_type = token_type - previous_text = text + +@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 after the + operator, not before it. + + W504: (width == 0 +\n height == 0) + W504: (width == 0 and\n height == 0) + + Okay: (width == 0\n + height == 0) + 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) + Okay: var = (1\n & ~2) + 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 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..34d6efe7 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 \ @@ -19,13 +18,13 @@ pass -if (foo == bar and - baz == frop): +if (foo == bar + and baz == frop): pass if ( - foo == bar and - baz == frop + foo == bar + and baz == frop ): pass From a3f7585d9135c5bef13b5a91f3358f4fabb83e0c Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Wed, 17 Jan 2018 22:42:22 -0600 Subject: [PATCH 06/22] Fix up testsuite for W504 --- pycodestyle.py | 24 ++++++++++++------------ setup.cfg | 2 +- testsuite/E12not.py | 29 ++++++++++++----------------- testsuite/W19.py | 18 +++++++++--------- testsuite/test_all.py | 2 +- testsuite/test_api.py | 4 ++-- 6 files changed, 37 insertions(+), 42 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index ad173829..f70a1a6e 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1184,16 +1184,15 @@ def break_before_binary_operator(logical_line, tokens): 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: (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) """ for context in _break_around_binary_operators(tokens): (token_type, text, previous_token_type, previous_text, @@ -1215,25 +1214,26 @@ def break_after_binary_operator(logical_line, tokens): W504: (width == 0 +\n height == 0) W504: (width == 0 and\n height == 0) + W504: var = (1 &\n ~2) - Okay: (width == 0\n + height == 0) 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) - Okay: var = (1\n & ~2) - Okay: var = (1\n / -2) - Okay: var = (1\n + -1\n + -2) + + 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)): + 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" diff --git a/setup.cfg b/setup.cfg index 91ea6742..2cd4b2ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,5 +6,5 @@ license_file = LICENSE [pycodestyle] select = -ignore = E226,E24 +ignore = E226,E24,W504 max_line_length = 79 diff --git a/testsuite/E12not.py b/testsuite/E12not.py index 34d6efe7..ebaa078f 100644 --- a/testsuite/E12not.py +++ b/testsuite/E12not.py @@ -16,12 +16,11 @@ or y > 1 \ or x == 3: pass - - +#: W503 if (foo == bar and baz == frop): pass - +#: W503 if ( foo == bar and baz == frop @@ -108,7 +107,7 @@ 'BBB' \ 'iii' \ 'CCC' - +#: W504 W504 abricot = (3 + 4 + 5 + 6) @@ -137,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)) @@ -183,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"""): @@ -223,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/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/test_all.py b/testsuite/test_all.py index 08f9ea91..f571a7b8 100644 --- a/testsuite/test_all.py +++ b/testsuite/test_all.py @@ -43,7 +43,7 @@ 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) diff --git a/testsuite/test_api.py b/testsuite/test_api.py index 4d8b7b26..6eb9f041 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 From 660afc4234c477e1fec0eac66ee8a5ce18fb262e Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Wed, 17 Jan 2018 22:43:23 -0600 Subject: [PATCH 07/22] Add a changelog entry for W504 --- CHANGES.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0957be83..3ab8c791 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,8 @@ UNRELEASED New checks: +* Add W504 warning for checking that a break doesn't happen after a binar + operator. This check is ignored by default * Add W605 warning for invalid escape sequences in string literals 2.3.1 (2017-01-31) From c5b9c147d0a06f3e2ec1468c3047f1955e3190f8 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Fri, 19 Jan 2018 08:06:07 -0800 Subject: [PATCH 08/22] Fixed typo --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3ab8c791..af06645c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,7 +6,7 @@ UNRELEASED New checks: -* Add W504 warning for checking that a break doesn't happen after a binar +* Add W504 warning for checking that a break doesn't happen after a binary operator. This check is ignored by default * Add W605 warning for invalid escape sequences in string literals From 147c399c357a08fa0af982161024af66710e35e5 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Fri, 19 Jan 2018 08:07:14 -0800 Subject: [PATCH 09/22] Fixed wording in docstring --- pycodestyle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index f70a1a6e..d8dfcf49 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1209,8 +1209,8 @@ def break_after_binary_operator(logical_line, tokens): r""" Avoid breaks after binary operators. - The preferred place to break around a binary operator is after the - operator, not before it. + 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) From 80684c4c48716837b06a2b0d6cd421851b1a0ae6 Mon Sep 17 00:00:00 2001 From: Peter Cock Date: Tue, 23 Jan 2018 10:37:54 +0000 Subject: [PATCH 10/22] Describe code W504 line break after binary operator Cross reference changes in pull request #502 which added this. --- docs/intro.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 4ddf91f4..854dc543 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -402,7 +402,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                         | +------------+----------------------------------------------------------------------+ +------------+----------------------------------------------------------------------+ | **W6** | *Deprecation warning* | @@ -420,9 +422,10 @@ This is the current list of error and warning codes: **(*)** 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 +**E133**, **E226**, **E241**, **E242**, **E704**, **W503** and **W504** are ignored +because they are not rules unanimously accepted, and `PEP 8`_ does not enforce them. +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**. **(^)** These checks can be disabled at the line level using the ``# noqa`` From 01ecae5a87d2bdf7df6528722e0c2ae36930d90f Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Thu, 8 Feb 2018 11:07:03 -0800 Subject: [PATCH 11/22] Updated License / copyright date --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 368e62cb6c57ff386b5a08659a5a9d2866b80a2f Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sat, 24 Mar 2018 13:06:24 +0000 Subject: [PATCH 12/22] Finalize support for Python 3.7 Python 3.7 added a warning for a future feature of nested regular expressions. To avoid this warning we escape what is not a nested regex. This also keeps track of the `async` keyword and handles it appropriately. Closes gh-728 --- pycodestyle.py | 4 ++-- testsuite/E25.py | 2 +- testsuite/E30.py | 2 +- testsuite/E70.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index 5f021870..3e574c81 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -102,7 +102,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(['^', '&', '|', '<<', '>>', '%']) @@ -121,7 +121,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') diff --git a/testsuite/E25.py b/testsuite/E25.py index 71d3f800..55ed4389 100644 --- a/testsuite/E25.py +++ b/testsuite/E25.py @@ -39,7 +39,7 @@ 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 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/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 From 9f225ac876b6340825770c9c665060dfc28300c6 Mon Sep 17 00:00:00 2001 From: Jimi Cullen Date: Tue, 3 Apr 2018 12:24:38 +0100 Subject: [PATCH 13/22] Ignore length of shebang line (#736) * Add special case to maximum_line_length to ignore long shebang lines. * Add test for ignoring long shebang lines. * Clean up shebang line check. --- pycodestyle.py | 6 +++++- testsuite/E50.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pycodestyle.py b/pycodestyle.py index 3e574c81..5371a67e 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -260,7 +260,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 @@ -275,6 +276,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. chunks = line.split() diff --git a/testsuite/E50.py b/testsuite/E50.py index 189f416a..6bca09bf 100644 --- a/testsuite/E50.py +++ b/testsuite/E50.py @@ -121,3 +121,7 @@ def foo(): #: E501 # This # almost_empty_line + +# +#: Okay +#!/reallylongpath/toexecutable --maybe --with --some ARGUMENTS TO DO WITH WHAT EXECUTABLE TO RUN From 5d31e7ee2e5996be10a0452d4b01b799a6698ad5 Mon Sep 17 00:00:00 2001 From: Adi Roiban Date: Sun, 8 Apr 2018 16:19:11 +0100 Subject: [PATCH 14/22] Add variables so blank lines may be configures This adds some module level configuration points for users to define how many blank lines they want in their code. It paves the way for someone to develop a flake8 plugin to configure this in pycodestyle. Fixes #732 --- CONTRIBUTING.rst | 38 +++ pycodestyle.py | 48 ++- testsuite/support.py | 20 ++ testsuite/test_all.py | 10 +- testsuite/test_blank_lines.py | 552 ++++++++++++++++++++++++++++++++++ 5 files changed, 653 insertions(+), 15 deletions(-) create mode 100644 testsuite/test_blank_lines.py 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/pycodestyle.py b/pycodestyle.py index 5371a67e..96779ab9 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -95,6 +95,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, +} REPORT_FORMAT = { 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', @@ -332,37 +339,50 @@ def blank_lines(logical_line, blank_lines, indent_level, line_number, E305: def a():\n pass\na() E306: def a():\n def b():\n pass\n def c():\n pass """ - if line_number < 3 and not previous_logical: + 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 diff --git a/testsuite/support.py b/testsuite/support.py index cf9abc53..825def14 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 f571a7b8..0e4bc7d1 100644 --- a/testsuite/test_all.py +++ b/testsuite/test_all.py @@ -48,11 +48,19 @@ def test_own_dog_food(self): 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_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) From 41438bf9a8ccebf392bd005237e845b6610e7e9b Mon Sep 17 00:00:00 2001 From: Jakob Gerhard Martinussen Date: Mon, 9 Apr 2018 20:22:19 +0200 Subject: [PATCH 15/22] Add changelog items for 2.3.1...master --- CHANGES.txt | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 25b8558a..e86aa99a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,10 +7,46 @@ UNRELEASED New checks: * Add W504 warning for checking that a break doesn't happen after a binary - operator. This check is ignored by default -* Add W605 warning for invalid escape sequences in string literals + operator. This check is ignored by default. +* Add W605 warning for invalid escape sequences in string literals. * Add W606 warning for 'async' and 'await' reserved keywords being introduced - in Python 3.7 + in Python 3.7. +* Add E252 error for missing whitespace around equal sign in type annotated + function arguments with defaults values. + +Changes: + +* An internal bisect search has replaced a linear search in order to improve + efficiency. +* pycodestyle now uses PyPI trove classifiers in order to document supported + python versions on PyPI. +* 'setup.cfg' '[wheel]' section has been renamed to '[bdist_wheel]', as + the former is legacy. +* pycodestyle now handles very long lines much more efficiently for python + 3.2+. Fixes #643. +* You can now write 'pycodestyle.StyleGuide(verbose=True)' instead of + 'pycodestyle.StyleGuide(verbose=True, paths=['-v'])' in order to achieve + verbosity. +* The distribution of pycodestyle now includes the licence text in order to + comply with open source licenses which require this. +* 'maximum_line_length' now ignores shebang ('#!') lines. +* 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. + +Bugs: + +* Prevent a 'DeprecationWarning', and a 'SyntaxError' in future python, caused + by an invalid escape sequence. +* Correctly report E501 when the first line of a docstring is too long. + Resolves #622. +* Support variable annotation when variable start by a keyword, such as class + variable type annotations in python 3.6. +* pycodestyle internals have been changed in order to allow 'python3 -m + cProfile' to report correct metrics. +* Fix a spelling mistake in the description of E722. +* 'pycodestyle --diff' now does not break if your 'gitconfig' enables + 'mnemonicprefix'. 2.3.1 (2017-01-31) ------------------ From 6d10bc73906144d27928d1be306032393f658a80 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Mon, 9 Apr 2018 12:01:37 -0700 Subject: [PATCH 16/22] Fixed typo --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e86aa99a..daa77fd7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,7 +27,7 @@ Changes: * You can now write 'pycodestyle.StyleGuide(verbose=True)' instead of 'pycodestyle.StyleGuide(verbose=True, paths=['-v'])' in order to achieve verbosity. -* The distribution of pycodestyle now includes the licence text in order to +* The distribution of pycodestyle now includes the license text in order to comply with open source licenses which require this. * 'maximum_line_length' now ignores shebang ('#!') lines. * Add configuration option for the allowed number of blank lines. It is From 51cf479586cf7f4f47e66f30739f93714b45136a Mon Sep 17 00:00:00 2001 From: Jakob Gerhard Martinussen Date: Mon, 9 Apr 2018 21:22:19 +0200 Subject: [PATCH 17/22] Add PR references from all changelog items --- CHANGES.txt | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index daa77fd7..48905f9a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,46 +7,46 @@ UNRELEASED New checks: * Add W504 warning for checking that a break doesn't happen after a binary - operator. This check is ignored by default. -* Add W605 warning for invalid escape sequences in string literals. + 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. + in Python 3.7. PR #684. * Add E252 error for missing whitespace around equal sign in type annotated - function arguments with defaults values. + function arguments with defaults values. PR #717. Changes: * An internal bisect search has replaced a linear search in order to improve - efficiency. + efficiency. PR #648. * pycodestyle now uses PyPI trove classifiers in order to document supported - python versions on PyPI. + python versions on PyPI. PR #654. * 'setup.cfg' '[wheel]' section has been renamed to '[bdist_wheel]', as - the former is legacy. + the former is legacy. PR #653. * pycodestyle now handles very long lines much more efficiently for python - 3.2+. Fixes #643. + 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. + verbosity. PR #663. * The distribution of pycodestyle now includes the license text in order to - comply with open source licenses which require this. -* 'maximum_line_length' now ignores shebang ('#!') lines. + 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. + #732. PR #733. Bugs: * Prevent a 'DeprecationWarning', and a 'SyntaxError' in future python, caused - by an invalid escape sequence. + by an invalid escape sequence. PR #625. * Correctly report E501 when the first line of a docstring is too long. - Resolves #622. + Resolves #622. PR #630. * Support variable annotation when variable start by a keyword, such as class - variable type annotations in python 3.6. + 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. -* Fix a spelling mistake in the description of E722. + 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'. + 'mnemonicprefix'. PR #706. 2.3.1 (2017-01-31) ------------------ From f73c4bb211986cd8d340de2f018c3ff6a3d73c5f Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Tue, 10 Apr 2018 06:26:30 -0500 Subject: [PATCH 18/22] Release v2.4.0 --- CHANGES.txt | 4 ++-- pycodestyle.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 48905f9a..ecfa609a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,8 +1,8 @@ Changelog ========= -UNRELEASED ----------- +2.4.0 (2018-04-10) +------------------ New checks: diff --git a/pycodestyle.py b/pycodestyle.py index 96779ab9..1c8c5d27 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -77,7 +77,7 @@ 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,W504' From 25221c95fd180fbc30fb17de2c1930386348ac51 Mon Sep 17 00:00:00 2001 From: hhatto Date: Thu, 12 Apr 2018 23:28:51 +0900 Subject: [PATCH 19/22] Change the position pointed out by W605 --- pycodestyle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycodestyle.py b/pycodestyle.py index 1c8c5d27..da61e196 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1524,7 +1524,7 @@ 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], ) From fac287120f1672b06bc63065feed5f20a34f95f2 Mon Sep 17 00:00:00 2001 From: Ian Stapleton Cordasco Date: Thu, 12 Apr 2018 16:56:03 -0500 Subject: [PATCH 20/22] Fix flake8 test environment --- pycodestyle.py | 6 +++--- tox.ini | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index 1c8c5d27..b69ce33d 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -1689,9 +1689,9 @@ def parse_udiff(diff, patterns=None, parent='.'): 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/tox.ini b/tox.ini index 797bbfab..41ba1789 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 = From 17baca469a221da6b8c1531d13cfd33cb6eceed3 Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Wed, 18 Apr 2018 23:28:35 -0400 Subject: [PATCH 21/22] Fix detection of annotated argument defaults This improves E252 to allow default arguments like `_default(f=1)`. Closes gh-753 --- pycodestyle.py | 12 ++++++------ testsuite/E25.py | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pycodestyle.py b/pycodestyle.py index dc86d30d..f545cc0e 100755 --- a/pycodestyle.py +++ b/pycodestyle.py @@ -937,17 +937,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 == '=': - if not annotated_func_arg: - no_space = True - if start != prev_end: - yield (prev_end, message) - else: + 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 diff --git a/testsuite/E25.py b/testsuite/E25.py index 55ed4389..88981576 100644 --- a/testsuite/E25.py +++ b/testsuite/E25.py @@ -45,3 +45,6 @@ async def add(a: int = 0, b: int = 0) -> int: #: 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 From 11ba4cc825e3753a5b662efe41942b6b3021d7e5 Mon Sep 17 00:00:00 2001 From: Toshiki Hirao Date: Sun, 22 Apr 2018 16:00:41 -0400 Subject: [PATCH 22/22] Add explanation about the default configuration Closes gh-685 --- docs/intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/intro.rst b/docs/intro.rst index 80ef08f4..70e7e81d 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -426,6 +426,8 @@ This is the current list of error and warning codes: **(*)** In the default configuration, the checks **E121**, **E123**, **E126**, **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**.