File: imports.py

package info (click to toggle)
flufl.testing 0.8-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 144 kB
  • sloc: python: 277; makefile: 5
file content (128 lines) | stat: -rw-r--r-- 5,414 bytes parent folder | download
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