diff options
-rw-r--r-- | CHANGES.txt | 2 | ||||
-rw-r--r-- | docs/developer.rst | 6 | ||||
-rw-r--r-- | docs/intro.rst | 2 | ||||
-rwxr-xr-x | pep8.py | 56 | ||||
-rw-r--r-- | testsuite/E12not.py | 2 |
5 files changed, 67 insertions, 1 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index bb875a9..25222ab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -30,6 +30,8 @@ Bug fixes: * Add ``.tox/`` to default excludes. (Issue #335) +* Report E402 for import statements not at the top of the file. (Issue #264) + 1.5.7 (2014-05-29) ------------------ 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/intro.rst b/docs/intro.rst index 7d05cd8..1bcafe8 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -324,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* | +----------+----------------------------------------------------------------------+ @@ -839,6 +839,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. @@ -1251,6 +1298,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 [] @@ -1306,10 +1355,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 @@ -1368,6 +1423,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..995a368 100644 --- a/testsuite/E12not.py +++ b/testsuite/E12not.py @@ -632,7 +632,7 @@ some_hash = { else 0, } # -from textwrap import dedent +from textwrap import dedent # noqa print dedent( |