summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--CHANGES.txt18
-rw-r--r--docs/conf.py9
-rw-r--r--docs/developer.rst6
-rw-r--r--docs/index.rst2
-rw-r--r--docs/intro.rst31
-rwxr-xr-xpep8.py84
-rw-r--r--testsuite/E12not.py2
-rw-r--r--testsuite/E71.py17
-rw-r--r--testsuite/test_all.py3
-rw-r--r--testsuite/test_api.py42
-rw-r--r--testsuite/test_util.py23
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* |
+----------+----------------------------------------------------------------------+
diff --git a/pep8.py b/pep8.py
index 8e16faf..87e2881 100755
--- a/pep8.py
+++ b/pep8.py
@@ -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/*'])