diff options
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | CHANGES.txt | 18 | ||||
-rw-r--r-- | docs/conf.py | 9 | ||||
-rw-r--r-- | docs/developer.rst | 6 | ||||
-rw-r--r-- | docs/index.rst | 2 | ||||
-rw-r--r-- | docs/intro.rst | 31 | ||||
-rwxr-xr-x | pep8.py | 84 | ||||
-rw-r--r-- | testsuite/E12not.py | 2 | ||||
-rw-r--r-- | testsuite/E71.py | 17 | ||||
-rw-r--r-- | testsuite/test_all.py | 3 | ||||
-rw-r--r-- | testsuite/test_api.py | 42 | ||||
-rw-r--r-- | testsuite/test_util.py | 23 |
12 files changed, 225 insertions, 16 deletions
diff --git a/.travis.yml b/.travis.yml index c50362b..a37c115 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,7 @@ script: matrix: allow_failures: - python: pypy + +notifications: + email: + - IanLee1521@gmail.com diff --git a/CHANGES.txt b/CHANGES.txt index 4bcfcf7..f506dc9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,12 @@ Changelog 1.x (unreleased) ---------------- +News: + +* Ian Lee <ianlee1521@gmail.com> joined the project as a maintainer. + +Changes: + * Report E731 for lambda assignment. (Issue #277) * Report E704 for one-liner def instead of E701. @@ -16,6 +22,18 @@ Changelog * Report E266 instead of E265 when the block comment starts with multiple ``#``. (Issue #270) +* Report E402 for import statements not at the top of the file. (Issue #264) + +* Strip whitespace from around paths during normalization. (Issue #339 / #343) + +* Update ``--format`` documentation. (Issue #198 / Pull Request #310) + +* Add ``.tox/`` to default excludes. (Issue #335) + +Bug fixes: + +* Don't crash if Checker.build_tokens_line() returns None. (Issue #306) + 1.5.7 (2014-05-29) ------------------ diff --git a/docs/conf.py b/docs/conf.py index cd288cc..d1dca3c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,8 @@ master_doc = 'index' # General information about the project. project = u'pep8' -copyright = u'2012-2013, Florent Xicluna' +authors = u'Johann C. Rocholl, Florent Xicluna, Ian Lee' +copyright = u'2006-2014, %s' % (authors) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -192,7 +193,7 @@ latex_elements = { # author, documentclass [howto/manual]). latex_documents = [ ('index', 'pep8.tex', u'pep8 documentation', - u'Florent Xicluna', 'manual'), + authors, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -222,7 +223,7 @@ latex_documents = [ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pep8', u'pep8 documentation', - [u'Florent Xicluna'], 1) + [authors], 1) ] # If true, show URL addresses after external links. @@ -235,7 +236,7 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'pep8', u'pep8 documentation', u'Florent Xicluna', + ('index', 'pep8', u'pep8 documentation', authors, 'pep8', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/developer.rst b/docs/developer.rst index 7df1734..4725acd 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -59,6 +59,12 @@ additional information with extra arguments. All attributes of the * ``previous_indent_level``: indentation on previous line * ``previous_logical``: previous logical line +Check plugins can also maintain per-file state. If you need this, declare +a parameter named ``checker_state``. You will be passed a dict, which will be +the same one for all lines in the same file but a different one for different +files. Each check plugin gets its own dict, so you don't need to worry about +clobbering the state of other plugins. + The docstring of each check function shall be the relevant part of text from `PEP 8`_. It is printed if the user enables ``--show-pep8``. Several docstrings contain examples directly from the `PEP 8`_ document. diff --git a/docs/index.rst b/docs/index.rst index eb3f21a..5500e0d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,7 +37,7 @@ Credits Created by Johann C. Rocholl. -Maintained by Florent Xicluna. +Maintained by Florent Xicluna and Ian Lee. .. _license: diff --git a/docs/intro.rst b/docs/intro.rst index 5e3af1f..1bcafe8 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -111,6 +111,33 @@ Or you can display how often each error was found:: 612 W601 .has_key() is deprecated, use 'in' 1188 W602 deprecated form of raising exception +You can also make pep8.py show the error text in different formats by using --format having options default/pylint/custom:: + + $ pep8 testsuite/E40.py --format=default + testsuite/E40.py:2:10: E401 multiple imports on one line + + $ pep8 testsuite/E40.py --format=pylint + testsuite/E40.py:2: [E401] multiple imports on one line + + $ pep8 testsuite/E40.py --format='%(path)s|%(row)d|%(col)d| %(code)s %(text)s' + testsuite/E40.py|2|10| E401 multiple imports on one line + +Variables in the ``custom`` format option + ++----------------+------------------+ +| Variable | Significance | ++================+==================+ +| ``path`` | File name | ++----------------+------------------+ +| ``row`` | Row number | ++----------------+------------------+ +| ``col`` | Column number | ++----------------+------------------+ +| ``code`` | Error code | ++----------------+------------------+ +| ``text`` | Error text | ++----------------+------------------+ + Quick help is available on the command line:: $ pep8 -h @@ -158,7 +185,7 @@ Configuration The behaviour may be configured at two levels. -The user settings are read from the ``~/.config/pep8`` file and +The user settings are read from the ``~/.config/pep8`` file and for Windows from the ``~\.pep8`` file. Example:: @@ -297,6 +324,8 @@ This is the current list of error and warning codes: +----------+----------------------------------------------------------------------+ | E401 | multiple imports on one line | +----------+----------------------------------------------------------------------+ +| E402 | module level import not at top of file | ++----------+----------------------------------------------------------------------+ +----------+----------------------------------------------------------------------+ | **E5** | *Line length* | +----------+----------------------------------------------------------------------+ @@ -2,6 +2,7 @@ # pep8.py - Check Python source code formatting, according to PEP 8 # Copyright (C) 2006-2009 Johann C. Rocholl <johann@rocholl.net> # Copyright (C) 2009-2014 Florent Xicluna <florent.xicluna@gmail.com> +# Copyright (C) 2014 Ian Lee <ianlee1521@gmail.com> # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -63,7 +64,7 @@ try: except ImportError: from ConfigParser import RawConfigParser -DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__' +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__,.tox' DEFAULT_IGNORE = 'E123,E226,E24,E704' if sys.platform == 'win32': DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') @@ -101,7 +102,10 @@ ERRORCODE_REGEX = re.compile(r'\b[A-Z]\d{3}\b') DOCSTRING_REGEX = re.compile(r'u?r?["\']') EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') -COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_SINGLETON_REGEX = re.compile(r'(?P<op>[=!]=)\s*' + r'(?P<singleton>None|False|True)') +COMPARE_SINGLETON_REVERSE_REGEX = re.compile(r'(?P<singleton>None|False|True)' + r'\s*(?P<op>[=!]=)') COMPARE_NEGATIVE_REGEX = re.compile(r'\b(not)\s+[^[({ ]+\s+(in|is)\s') COMPARE_TYPE_REGEX = re.compile(r'(?:[=!]=|is(?:\s+not)?)\s*type(?:s.\w+Type' r'|\s*\(\s*([^)]*[^ )])\s*\))') @@ -836,6 +840,53 @@ def imports_on_separate_lines(logical_line): yield found, "E401 multiple imports on one line" +def module_imports_on_top_of_file( + logical_line, indent_level, checker_state, noqa): + r"""Imports are always put at the top of the file, just after any module + comments and docstrings, and before module globals and constants. + + Okay: import os + Okay: # this is a comment\nimport os + Okay: '''this is a module docstring'''\nimport os + Okay: r'''this is a module docstring'''\nimport os + Okay: __version__ = "123"\nimport os + E402: a=1\nimport os + E402: 'One string'\n"Two string"\nimport os + E402: a=1\nfrom sys import x + + Okay: if x:\n import os + """ + def is_string_literal(line): + if line[0] in 'uUbB': + line = line[1:] + if line and line[0] in 'rR': + line = line[1:] + return line and (line[0] == '"' or line[0] == "'") + + if indent_level: # Allow imports in conditional statements or functions + return + if not logical_line: # Allow empty lines or comments + return + if noqa: + return + line = logical_line + if line.startswith('import ') or line.startswith('from '): + if checker_state.get('seen_non_imports', False): + yield 0, "E402 import not at top of file" + elif line.startswith('__version__ '): + # These lines should be included after the module's docstring, before + # any other code, separated by a blank line above and below. + return + elif is_string_literal(line): + # The first literal is a docstring, allow it. Otherwise, report error. + if checker_state.get('seen_docstring', False): + checker_state['seen_non_imports'] = True + else: + checker_state['seen_docstring'] = True + else: + checker_state['seen_non_imports'] = True + + def compound_statements(logical_line): r"""Compound statements (on the same line) are generally discouraged. @@ -933,17 +984,22 @@ def comparison_to_singleton(logical_line, noqa): Okay: if arg is not None: E711: if arg != None: + E711: if None == arg: E712: if arg == True: + E712: if False == arg: Also, beware of writing if x when you really mean if x is not None -- e.g. when testing whether a variable or argument that defaults to None was set to some other value. The other value might have a type (such as a container) that could be false in a boolean context! """ - match = not noqa and COMPARE_SINGLETON_REGEX.search(logical_line) + + match = not noqa and (COMPARE_SINGLETON_REGEX.search(logical_line) or + COMPARE_SINGLETON_REVERSE_REGEX.search(logical_line)) if match: - same = (match.group(1) == '==') - singleton = match.group(2) + singleton = match.group('singleton') + same = (match.group('op') == '==') + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) if singleton in ('None',): code = 'E711' @@ -1158,10 +1214,13 @@ def normalize_paths(value, parent=os.curdir): Return a list of absolute paths. """ - if not value or isinstance(value, list): + if not value: + return [] + if isinstance(value, list): return value paths = [] for path in value.split(','): + path = path.strip() if '/' in path: path = os.path.abspath(os.path.join(parent, path)) paths.append(path.rstrip('/')) @@ -1242,6 +1301,8 @@ class Checker(object): self.hang_closing = options.hang_closing self.verbose = options.verbose self.filename = filename + # Dictionary where a checker can store its custom state. + self._checker_states = {} if filename is None: self.filename = 'stdin' self.lines = lines or [] @@ -1297,10 +1358,16 @@ class Checker(object): arguments.append(getattr(self, name)) return check(*arguments) + def init_checker_state(self, name, argument_names): + """ Prepares a custom state for the specific checker plugin.""" + if 'checker_state' in argument_names: + self.checker_state = self._checker_states.setdefault(name, {}) + def check_physical(self, line): """Run all physical checks on a raw input line.""" self.physical_line = line for name, check, argument_names in self._physical_checks: + self.init_checker_state(name, argument_names) result = self.run_check(check, argument_names) if result is not None: (offset, text) = result @@ -1345,6 +1412,10 @@ class Checker(object): """Build a line from tokens and run all logical checks on it.""" self.report.increment_logical_line() mapping = self.build_tokens_line() + + if not mapping: + return + (start_row, start_col) = mapping[0][1] start_line = self.lines[start_row - 1] self.indent_level = expand_indent(start_line[:start_col]) @@ -1355,6 +1426,7 @@ class Checker(object): for name, check, argument_names in self._logical_checks: if self.verbose >= 4: print(' ' + name) + self.init_checker_state(name, argument_names) for offset, text in self.run_check(check, argument_names) or (): if not isinstance(offset, tuple): for token_offset, pos in mapping: diff --git a/testsuite/E12not.py b/testsuite/E12not.py index a53d9a4..e76ef13 100644 --- a/testsuite/E12not.py +++ b/testsuite/E12not.py @@ -631,8 +631,6 @@ some_hash = { 999999 if True else 0, } -# -from textwrap import dedent print dedent( diff --git a/testsuite/E71.py b/testsuite/E71.py index 2ff5dea..3f07b1a 100644 --- a/testsuite/E71.py +++ b/testsuite/E71.py @@ -1,12 +1,29 @@ #: E711 if res == None: pass +#: E711 +if res != None: + pass +#: E711 +if None == res: + pass +#: E711 +if None != res: + pass + +# #: E712 if res == True: pass #: E712 if res != False: pass +#: E712 +if True != res: + pass +#: E712 +if False == res: + pass # #: E713 diff --git a/testsuite/test_all.py b/testsuite/test_all.py index 5160900..50e2cb9 100644 --- a/testsuite/test_all.py +++ b/testsuite/test_all.py @@ -46,12 +46,13 @@ class Pep8TestCase(unittest.TestCase): def suite(): - from testsuite import test_api, test_shell + from testsuite import test_api, test_shell, test_util suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(Pep8TestCase)) suite.addTest(unittest.makeSuite(test_api.APITestCase)) suite.addTest(unittest.makeSuite(test_shell.ShellTestCase)) + suite.addTest(unittest.makeSuite(test_util.UtilTestCase)) return suite diff --git a/testsuite/test_api.py b/testsuite/test_api.py index 672f202..de7bc7b 100644 --- a/testsuite/test_api.py +++ b/testsuite/test_api.py @@ -160,7 +160,8 @@ class APITestCase(unittest.TestCase): ['directories', 'files', 'logical lines', 'physical lines']) self.assertEqual(pep8style.options.exclude, - ['.svn', 'CVS', '.bzr', '.hg', '.git', '__pycache__']) + ['.svn', 'CVS', '.bzr', '.hg', + '.git', '__pycache__', '.tox']) self.assertEqual(pep8style.options.filename, ['*.py']) self.assertEqual(pep8style.options.format, 'default') self.assertEqual(pep8style.options.select, ()) @@ -233,6 +234,7 @@ class APITestCase(unittest.TestCase): self.assertFalse(pep8style.excluded('./foo/bar/main.py')) self.assertTrue(pep8style.excluded('./CVS')) + self.assertTrue(pep8style.excluded('./.tox')) self.assertTrue(pep8style.excluded('./subdir/CVS')) self.assertTrue(pep8style.excluded('__pycache__')) self.assertTrue(pep8style.excluded('./__pycache__')) @@ -342,5 +344,43 @@ class APITestCase(unittest.TestCase): self.assertFalse(sys.stderr) self.assertEqual(count_errors, 1) + def test_styleguide_unmatched_triple_quotes(self): + pep8.register_check(DummyChecker, ['Z701']) + lines = [ + 'def foo():\n', + ' """test docstring""\'\n', + ] + + pep8style = pep8.StyleGuide() + pep8style.input_file('stdin', lines=lines) + stdout = sys.stdout.getvalue() + + expected = 'stdin:2:5: E901 TokenError: EOF in multi-line string' + self.assertTrue(expected in stdout) + + def test_styleguide_continuation_line_outdented(self): + pep8.register_check(DummyChecker, ['Z701']) + lines = [ + 'def foo():\n', + ' pass\n', + '\n', + '\\\n', + '\n', + 'def bar():\n', + ' pass\n', + ] + + pep8style = pep8.StyleGuide() + count_errors = pep8style.input_file('stdin', lines=lines) + self.assertEqual(count_errors, 2) + stdout = sys.stdout.getvalue() + expected = ( + 'stdin:6:1: ' + 'E122 continuation line missing indentation or outdented' + ) + self.assertTrue(expected in stdout) + expected = 'stdin:6:1: E302 expected 2 blank lines, found 1' + self.assertTrue(expected in stdout) + # TODO: runner # TODO: input_file diff --git a/testsuite/test_util.py b/testsuite/test_util.py new file mode 100644 index 0000000..11395cc --- /dev/null +++ b/testsuite/test_util.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import unittest + +import pep8 + + +class UtilTestCase(unittest.TestCase): + def test_normalize_paths(self): + cwd = os.getcwd() + + self.assertEqual(pep8.normalize_paths(''), []) + self.assertEqual(pep8.normalize_paths([]), []) + self.assertEqual(pep8.normalize_paths(None), []) + self.assertEqual(pep8.normalize_paths(['foo']), ['foo']) + self.assertEqual(pep8.normalize_paths('foo'), ['foo']) + self.assertEqual(pep8.normalize_paths('foo,bar'), ['foo', 'bar']) + self.assertEqual(pep8.normalize_paths('foo, bar '), ['foo', 'bar']) + self.assertEqual(pep8.normalize_paths('/foo/bar,baz/../bat'), + ['/foo/bar', cwd + '/bat']) + self.assertEqual(pep8.normalize_paths(".pyc,\n build/*"), + ['.pyc', cwd + '/build/*']) |