1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
|
"""flake8 plugin."""
from ast import NodeVisitor
from collections import namedtuple
from enum import Enum
class ImportType(Enum):
non_from = 0
from_import = 1
ImportRecord = namedtuple('ImportRecord', 'itype lineno colno, module, names')
NONFROM_FOLLOWS_FROM = 'U401 Non-from import follows from-import'
NONFROM_MULTIPLE_NAMES = 'U402 Multiple names on non-from import'
NONFROM_SHORTER_FOLLOWS = 'U403 Shorter non-from import follows longer'
NONFROM_ALPHA_UNSORTED = (
'U404 Same-length non-from imports not sorted alphabetically')
NONFROM_EXTRA_BLANK_LINE = (
'U405 Unexpected blank line since last non-from import')
NONFROM_DOTTED_UNSORTED = (
'U406 Dotted non-from import not sorted alphabetically')
FROMIMPORT_MISSING_BLANK_LINE = (
'U411 Expected one blank line since last non-from import')
FROMIMPORT_ALPHA_UNSORTED = 'U412 from-import not sorted alphabetically'
FROMIMPORT_MULTIPLE = 'U413 Multiple from-imports of same module'
FROMIMPORT_NAMES_UNSORTED = (
'U414 from-imported names are not sorted alphabetically')
class ImportVisitor(NodeVisitor):
def __init__(self):
self.imports = []
def visit_Import(self, node):
if node.col_offset != 0:
# Ignore nested imports.
return
names = [alias.name for alias in node.names]
self.imports.append(
ImportRecord(ImportType.non_from, node.lineno, node.col_offset,
None, names))
def visit_ImportFrom(self, node):
if node.col_offset != 0:
# Ignore nested imports.
return
names = [alias.name for alias in node.names]
self.imports.append(
ImportRecord(ImportType.from_import, node.lineno, node.col_offset,
node.module, names))
class ImportOrder:
name = 'flufl-import-order'
version = '0.2'
off_by_default = True
def __init__(self, tree, filename):
self.tree = tree
self.filename = filename
def _error(self, record, error):
code, space, text = error.partition(' ')
return (record.lineno, record.colno,
'{} {}'.format(code, text), ImportOrder)
def run(self):
visitor = ImportVisitor()
visitor.visit(self.tree)
last_import = None
for record in visitor.imports:
if last_import is None:
last_import = record
continue
if record.itype is ImportType.non_from:
if len(record.names) != 1:
yield self._error(record, NONFROM_MULTIPLE_NAMES)
if last_import.itype is ImportType.from_import:
# If the previous import was a __future__ import, just
# ignore the rest of the checks.
if last_import.module is '__future__':
continue
yield self._error(record, NONFROM_FOLLOWS_FROM)
# Shorter imports should always precede longer import *except*
# when they are dotted imports and everything but the last
# path component are the same. In that case, they should be
# sorted alphabetically.
last_name = last_import.names[0]
this_name = record.names[0]
if '.' in last_name and '.' in this_name:
last_parts = last_name.split('.')
this_parts = this_name.split('.')
if (last_parts[:-1] == this_parts[:-1] and
last_parts[-1] > this_parts[-1]):
yield self._error(record, NONFROM_DOTTED_UNSORTED)
elif len(last_name) > len(this_name):
yield self._error(record, NONFROM_SHORTER_FOLLOWS)
# It's also possible that the imports are the same length, in
# which case they must be sorted alphabetically.
if (len(last_import.names[0]) == len(record.names[0]) and
last_import.names[0] > record.names[0]):
yield self._error(record, NONFROM_ALPHA_UNSORTED)
if last_import.lineno + 1 != record.lineno:
yield self._error(record, NONFROM_DOTTED_UNSORTED)
else:
assert record.itype is ImportType.from_import
if (last_import.itype is ImportType.non_from and
record.lineno != last_import.lineno + 2):
yield self._error(record, FROMIMPORT_MISSING_BLANK_LINE)
if last_import.itype is ImportType.non_from:
last_import = record
continue
if last_import.module > record.module:
yield self._error(record, FROMIMPORT_ALPHA_UNSORTED)
# All imports from the same module should show up in the same
# multiline import.
if last_import.module == record.module:
yield self._error(record, FROMIMPORT_MULTIPLE)
# Check the sort order of the imported names.
if sorted(record.names) != record.names:
yield self._error(record, FROMIMPORT_NAMES_UNSORTED)
# How to check for no blank lines between from imports?
# Update the last import.
last_import = record
|