1from collections import namedtuple
2
3from pkg_resources import iter_entry_points
4
5from flake8_import_order import ClassifiedImport, ImportType, NewLine
6
7Error = namedtuple('Error', ['lineno', 'code', 'message'])
8
9
10def list_entry_points():
11    return iter_entry_points('flake8_import_order.styles')
12
13
14def lookup_entry_point(name):
15    try:
16        return next(iter_entry_points('flake8_import_order.styles', name=name))
17    except StopIteration:
18        raise LookupError('Unknown style {}'.format(name))
19
20
21class Style(object):
22
23    accepts_application_package_names = False
24
25    def __init__(self, nodes):
26        self.nodes = nodes
27
28    def check(self):
29        previous = None
30        previous_import = None
31        for current in self.nodes:
32            if isinstance(current, ClassifiedImport):
33                for error in self._check(previous_import, previous, current):
34                    yield error
35                previous_import = current
36            previous = current
37
38    def _check(self, previous_import, previous, current_import):
39        for error in self._check_I666(current_import):
40            yield error
41        for error in self._check_I101(current_import):
42            yield error
43        if previous_import is not None:
44            for error in self._check_I100(previous_import, current_import):
45                yield error
46            for error in self._check_I201(
47                    previous_import, previous, current_import,
48            ):
49                yield error
50            for error in self._check_I202(
51                    previous_import, previous, current_import,
52            ):
53                yield error
54
55    def _check_I666(self, current_import):  # noqa: N802
56        if current_import.type == ImportType.MIXED:
57            yield Error(
58                current_import.lineno,
59                'I666',
60                'Import statement mixes groups',
61            )
62
63    def _check_I101(self, current_import):  # noqa: N802
64        correct_names = self.sorted_names(current_import.names)
65        if correct_names != current_import.names:
66            corrected = ', '.join(correct_names)
67            yield Error(
68                current_import.lineno,
69                'I101',
70                "Imported names are in the wrong order. "
71                "Should be {0}".format(corrected),
72            )
73
74    def _check_I100(self, previous_import, current_import):  # noqa: N802
75        previous_key = self.import_key(previous_import)
76        current_key = self.import_key(current_import)
77        if previous_key > current_key:
78                message = (
79                    "Import statements are in the wrong order. "
80                    "'{0}' should be before '{1}'"
81                ).format(
82                    self._explain_import(current_import),
83                    self._explain_import(previous_import),
84                )
85                same_section = self.same_section(
86                    previous_import, current_import,
87                )
88                if not same_section:
89                    message = "{0} and in a different group.".format(message)
90                yield Error(current_import.lineno, 'I100', message)
91
92    def _check_I201(self, previous_import, previous, current_import):  # noqa: N802,E501
93        same_section = self.same_section(previous_import, current_import)
94        has_newline = isinstance(previous, NewLine)
95        if not same_section and not has_newline:
96            yield Error(
97                current_import.lineno,
98                'I201',
99                "Missing newline between import groups. {}".format(
100                    self._explain_grouping(
101                        current_import, previous_import,
102                    )
103                ),
104            )
105
106    def _check_I202(self, previous_import, previous, current_import):  # noqa: N802,E501
107        same_section = self.same_section(previous_import, current_import)
108        has_newline = isinstance(previous, NewLine)
109        if same_section and has_newline:
110            yield Error(
111                current_import.lineno,
112                'I202',
113                "Additional newline in a group of imports. {}".format(
114                    self._explain_grouping(
115                        current_import, previous_import,
116                    )
117                ),
118            )
119
120    @staticmethod
121    def sorted_names(names):
122        return names
123
124    @staticmethod
125    def import_key(import_):
126        return (import_.type,)
127
128    @staticmethod
129    def same_section(previous, current):
130        same_type = current.type == previous.type
131        both_first = (
132            {previous.type, current.type} <= {
133                ImportType.APPLICATION, ImportType.APPLICATION_RELATIVE,
134            }
135        )
136        return same_type or both_first
137
138    @staticmethod
139    def _explain_import(import_):
140        if import_.is_from:
141            return "from {}{} import {}".format(
142                import_.level * '.',
143                ', '.join(import_.modules),
144                ', '.join(import_.names),
145            )
146        else:
147            return "import {}".format(', '.join(import_.modules))
148
149    @staticmethod
150    def _explain_grouping(current_import, previous_import):
151        return (
152            "'{0}' is identified as {1} and "
153            "'{2}' is identified as {3}."
154        ).format(
155            Style._explain_import(current_import),
156            current_import.type.name.title().replace('_', ' '),
157            Style._explain_import(previous_import),
158            previous_import.type.name.title().replace('_', ' '),
159        )
160
161
162class PEP8(Style):
163    pass
164
165
166class Google(Style):
167
168    @staticmethod
169    def sorted_names(names):
170        return sorted(names, key=Google.name_key)
171
172    @staticmethod
173    def name_key(name):
174        return (name.lower(), name)
175
176    @staticmethod
177    def import_key(import_):
178        modules = [Google.name_key(module) for module in import_.modules]
179        names = [Google.name_key(name) for name in import_.names]
180        return (import_.type, import_.level, modules, names)
181
182
183class AppNexus(Google):
184    accepts_application_package_names = True
185
186
187class Smarkets(Style):
188
189    @staticmethod
190    def sorted_names(names):
191        return sorted(names, key=Smarkets.name_key)
192
193    @staticmethod
194    def name_key(name):
195        return (name.lower(), name)
196
197    @staticmethod
198    def import_key(import_):
199        modules = [Smarkets.name_key(module) for module in import_.modules]
200        names = [Smarkets.name_key(name) for name in import_.names]
201        return (import_.type, import_.is_from, import_.level, modules, names)
202
203
204class Edited(Smarkets):
205    accepts_application_package_names = True
206
207    def _check_I202(self, previous_import, previous, current_import):  # noqa: N802,E501
208        same_section = self.same_section(previous_import, current_import)
209        has_newline = isinstance(previous, NewLine)
210        optional_split = current_import.is_from and not previous_import.is_from
211        if same_section and has_newline and not optional_split:
212            yield Error(
213                current_import.lineno,
214                'I202',
215                "Additional newline in a group of imports. {}".format(
216                    self._explain_grouping(
217                        current_import, previous_import,
218                    )
219                ),
220            )
221
222    @staticmethod
223    def same_section(previous, current):
224        return current.type == previous.type
225
226
227class PyCharm(Smarkets):
228    @staticmethod
229    def sorted_names(names):
230        return sorted(names)
231
232    @staticmethod
233    def import_key(import_):
234        return (import_.type, import_.is_from, import_.level, import_.modules, import_.names)
235
236
237class Cryptography(Style):
238
239    @staticmethod
240    def sorted_names(names):
241        return sorted(names)
242
243    @staticmethod
244    def import_key(import_):
245        if import_.type in {ImportType.THIRD_PARTY, ImportType.APPLICATION}:
246            return (
247                import_.type, import_.package, import_.is_from,
248                import_.level, import_.modules, import_.names,
249            )
250        else:
251            return (
252                import_.type, '', import_.is_from, import_.level,
253                import_.modules, import_.names,
254            )
255
256    @staticmethod
257    def same_section(previous, current):
258        app_or_third = current.type in {
259            ImportType.THIRD_PARTY, ImportType.APPLICATION,
260        }
261        same_type = current.type == previous.type
262        both_relative = (
263            previous.type == current.type == ImportType.APPLICATION_RELATIVE
264        )
265        same_package = previous.package == current.package
266        return (
267            (not app_or_third and same_type or both_relative) or
268            (app_or_third and same_package)
269        )
270