Report E402 for imports not on top of file; issue #264. Also add ability to keep...
authorDmitry Jemerov <yole@google.com>
Thu, 24 Jul 2014 16:29:58 +0000 (18:29 +0200)
committerDmitry Jemerov <yole@google.com>
Thu, 24 Jul 2014 16:29:58 +0000 (18:29 +0200)
CHANGES.txt
docs/developer.rst
pep8.py
testsuite/E12not.py

index 4bcfcf76b417c56dea26ccd4bcf7383aea1dbcde..81adde20edf2f0293d7b513933db2e989d802b62 100644 (file)
@@ -16,6 +16,8 @@ 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)
+
 
 1.5.7 (2014-05-29)
 ------------------
index 7df1734a305e2d6adef1bc147e25082ce3978879..4725acd9e7c731722bffec2bba873f4f5ae4c95e 100644 (file)
@@ -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/pep8.py b/pep8.py
index b31a9781d05bce5b30f19f070e5f936a58133189..2cd38d4eaa486e0ca5f2705dcd025cf397d6ac95 100755 (executable)
--- a/pep8.py
+++ b/pep8.py
@@ -835,6 +835,52 @@ def imports_on_separate_lines(logical_line):
             yield found, "E401 multiple imports on one line"
 
 
+def 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.
 
@@ -1239,6 +1285,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 []
@@ -1294,10 +1342,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
@@ -1352,6 +1406,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:
index a53d9a4c37f975e3769942a43ab26aa66f6669af..995a36863aa223be2ddea42842a9ec4a241282c5 100644 (file)
@@ -632,7 +632,7 @@ some_hash = {
         else 0,
 }
 #
-from textwrap import dedent
+from textwrap import dedent  # noqa
 
 
 print dedent(