11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
1 |
#!/usr/bin/python
|
2 |
#
|
|
3 |
# Copyright 2010 Canonical Ltd. This software is licensed under the
|
|
4 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
5 |
||
6 |
""" Format import sections in python files
|
|
7 |
||
8 |
= Usage =
|
|
9 |
||
10 |
format-imports <file or directory> ...
|
|
11 |
||
12 |
= Operation =
|
|
13 |
||
14 |
The script will process each filename on the command line. If the file is a
|
|
15 |
directory it recurses into it an process all *.py files found in the tree.
|
|
16 |
It will output the paths of all the files that have been changed.
|
|
17 |
||
11461.2.3
by Henning Eggers
Usage on the LP tree. |
18 |
For Launchpad it was applied to the "lib/canonical/launchpad" and the "lib/lp"
|
19 |
subtrees. Running it with those parameters on a freshly branched LP tree
|
|
20 |
should not produce any output, meaning that all the files in the tree should
|
|
21 |
be formatted correctly.
|
|
22 |
||
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
23 |
The script identifies the import section of each file as a block of lines
|
24 |
that start with "import" or "from" or are indented with at least one space or
|
|
25 |
are blank lines. Comment lines are also included if they are followed by an
|
|
26 |
import statement. An inital __future__ import and a module docstring are
|
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
27 |
explicitly skipped.
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
28 |
|
29 |
The import section is rewritten as three subsections, each separated by a
|
|
30 |
blank line. Any of the sections may be empty.
|
|
31 |
1. Standard python library modules
|
|
32 |
2. Import statements explicitly ordered to the top (see below)
|
|
33 |
3. Third-party modules, meaning anything not fitting one of the other
|
|
34 |
subsection criteria
|
|
35 |
4. Local modules that begin with "canonical" or "lp".
|
|
36 |
||
37 |
Each section is sorted alphabetically by module name. Each module is put
|
|
38 |
on its own line, i.e.
|
|
39 |
{{{
|
|
40 |
import os, sys
|
|
41 |
}}}
|
|
42 |
becomes
|
|
43 |
{{{
|
|
44 |
import os
|
|
45 |
import sys
|
|
46 |
}}}
|
|
47 |
Multiple import statements for the same module are conflated into one
|
|
48 |
statement, or two if the module was imported alongside an object inside it,
|
|
49 |
i.e.
|
|
50 |
{{{
|
|
51 |
import sys
|
|
52 |
from sys import stdin
|
|
53 |
}}}
|
|
54 |
||
55 |
Statements that import more than one objects are put on multiple lines in
|
|
56 |
list style, i.e.
|
|
57 |
{{{
|
|
58 |
from sys import (
|
|
59 |
stdin,
|
|
60 |
stdout,
|
|
61 |
)
|
|
62 |
}}}
|
|
63 |
Objects are sorted alphabetically and case-insensitively. One-object imports
|
|
64 |
are only formatted in this manner if the statement exceeds 78 characters in
|
|
65 |
length.
|
|
66 |
||
67 |
Comments stick with the import statement that followed them. Comments at the
|
|
68 |
end of one-line statements are moved to be be in front of it, .i.e.
|
|
69 |
{{{
|
|
70 |
from sys import exit # Have a way out
|
|
71 |
}}}
|
|
72 |
becomes
|
|
73 |
{{{
|
|
74 |
# Have a way out
|
|
75 |
from sys import exit
|
|
76 |
}}}
|
|
77 |
||
78 |
= Format control =
|
|
79 |
||
11461.2.4
by Henning Eggers
Reviewer comments. |
80 |
Two special comments allow to control the operation of the formatter.
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
81 |
|
11461.2.4
by Henning Eggers
Reviewer comments. |
82 |
When an import statement is immediately preceded by a comment that starts
|
83 |
with the word "FIRST", it is placed into the second subsection (see above).
|
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
84 |
|
85 |
When the first import statement is directly preceded by a comment that starts
|
|
86 |
with the word "SKIP", the entire file is exempt from formatting.
|
|
87 |
||
88 |
= Known bugs =
|
|
89 |
||
90 |
Make sure to always check the result of the re-formatting to see if you have
|
|
91 |
been bitten by one of these.
|
|
92 |
||
93 |
Comments inside multi-line import statements break the formatter. A statement
|
|
94 |
like this will be ignored:
|
|
95 |
{{{
|
|
96 |
from lp.app.interfaces import (
|
|
97 |
# Don't do this.
|
|
98 |
IMyInterface,
|
|
99 |
IMyOtherInterface, # Don't do this either
|
|
100 |
)
|
|
101 |
}}}
|
|
102 |
Actually, this will make the statement and all following to be ignored:
|
|
103 |
{{{
|
|
104 |
from lp.app.interfaces import (
|
|
105 |
# Breaks indentation rules anyway.
|
|
106 |
IMyInterface,
|
|
107 |
IMyOtherInterface,
|
|
108 |
)
|
|
109 |
}}}
|
|
110 |
||
111 |
If a single-line statement has both a comment in front of it and at the end
|
|
112 |
of the line, only the end-line comment will survive. This could probably
|
|
113 |
easily be fixed to concatenate the too.
|
|
114 |
{{{
|
|
115 |
# I am a gonner.
|
|
116 |
from lp.app.interfaces import IMyInterface # I will survive!
|
|
117 |
}}}
|
|
118 |
||
119 |
Line continuation characters are recognized and resolved but
|
|
120 |
not re-introduced. This may leave the re-formatted text with a line that
|
|
121 |
is over the length limit.
|
|
122 |
{{{
|
|
123 |
from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \
|
|
124 |
import object
|
|
125 |
}}}
|
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
126 |
"""
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
127 |
|
128 |
__metaclass__ = type |
|
129 |
||
130 |
# SKIP this file when reformatting.
|
|
131 |
import os |
|
132 |
import re |
|
133 |
import sys |
|
11461.2.2
by Henning Eggers
Made documentation easily available. |
134 |
from textwrap import dedent |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
135 |
|
136 |
sys.path[0:0] = [os.path.dirname(__file__)] |
|
137 |
from python_standard_libs import python_standard_libs |
|
138 |
||
12855.1.3
by Gavin Panella
Convert python_standard_libs into a frozenset in format-imports because it's only ever used for membership tests. |
139 |
# python_standard_libs is only used for membership tests.
|
140 |
python_standard_libs = frozenset(python_standard_libs) |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
141 |
|
142 |
# To search for escaped newline chars.
|
|
143 |
escaped_nl_regex = re.compile("\\\\\n", re.M) |
|
144 |
import_regex = re.compile("^import +(?P<module>.+)$", re.M) |
|
145 |
from_import_single_regex = re.compile( |
|
146 |
"^from (?P<module>.+) +import +"
|
|
147 |
"(?P<objects>[*]|[a-zA-Z0-9_, ]+)"
|
|
148 |
"(?P<comment>#.*)?$", re.M) |
|
149 |
from_import_multi_regex = re.compile( |
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
150 |
"^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", |
151 |
re.M) |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
152 |
comment_regex = re.compile( |
153 |
"(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M) |
|
154 |
split_regex = re.compile(",\s*") |
|
155 |
||
156 |
# Module docstrings are multiline (""") strings that are not indented and are
|
|
157 |
# followed at some point by an import .
|
|
158 |
module_docstring_regex = re.compile( |
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
159 |
'(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', |
160 |
re.M | re.S) |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
161 |
# The imports section starts with an import state that is not a __future__
|
162 |
# import and consists of import lines, indented lines, empty lines and
|
|
163 |
# comments which are followed by an import line. Sometimes we even find
|
|
164 |
# lines that contain a single ")"... :-(
|
|
165 |
imports_section_regex = re.compile( |
|
166 |
"(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n" |
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
167 |
"(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) " |
168 |
".+\n)|^\n|^[)]\n)*", |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
169 |
re.M) |
170 |
||
171 |
||
172 |
def format_import_lines(module, objects): |
|
173 |
"""Generate correct from...import strings."""
|
|
174 |
if len(objects) == 1: |
|
175 |
statement = "from %s import %s" % (module, objects[0]) |
|
176 |
if len(statement) < 79: |
|
177 |
return statement |
|
178 |
return "from %s import (\n %s,\n )" % ( |
|
179 |
module, ",\n ".join(objects)) |
|
180 |
||
181 |
||
182 |
def find_imports_section(content): |
|
183 |
"""Return that part of the file that contains the import statements."""
|
|
184 |
# Skip module docstring.
|
|
185 |
match = module_docstring_regex.search(content) |
|
186 |
if match is None: |
|
187 |
startpos = 0 |
|
188 |
else: |
|
189 |
startpos = match.end('docstring') |
|
190 |
||
191 |
match = imports_section_regex.search(content, startpos) |
|
192 |
if match is None: |
|
193 |
return (None, None) |
|
194 |
startpos = match.start() |
|
195 |
endpos = match.end() |
|
196 |
if content[startpos:endpos].startswith('# SKIP'): |
|
197 |
# Skip files explicitely.
|
|
198 |
return(None, None) |
|
199 |
return (startpos, endpos) |
|
200 |
||
201 |
||
202 |
class ImportStatement: |
|
203 |
"""Holds information about an import statement."""
|
|
204 |
||
205 |
def __init__(self, objects=None, comment=None): |
|
206 |
self.import_module = objects is None |
|
207 |
if objects is None: |
|
208 |
self.objects = None |
|
209 |
else: |
|
210 |
self.objects = sorted(objects, key=str.lower) |
|
211 |
self.comment = comment |
|
212 |
||
213 |
def addObjects(self, new_objects): |
|
214 |
"""More objects in this statement; eliminate duplicates."""
|
|
215 |
if self.objects is None: |
|
216 |
# No objects so far.
|
|
217 |
self.objects = new_objects |
|
218 |
else: |
|
219 |
# Use set to eliminate double objects.
|
|
220 |
more_objects = set(self.objects + new_objects) |
|
221 |
self.objects = sorted(list(more_objects), key=str.lower) |
|
222 |
||
223 |
def setComment(self, comment): |
|
224 |
"""Add a comment to the statement."""
|
|
225 |
self.comment = comment |
|
226 |
||
227 |
||
228 |
def parse_import_statements(import_section): |
|
229 |
"""Split the import section into statements.
|
|
230 |
||
231 |
Returns a dictionary with the module as the key and the objects being
|
|
232 |
imported as a sorted list of strings."""
|
|
233 |
imports = {} |
|
234 |
# Search for escaped newlines and remove them.
|
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
235 |
searchpos = 0 |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
236 |
while True: |
237 |
match = escaped_nl_regex.search(import_section, searchpos) |
|
238 |
if match is None: |
|
239 |
break
|
|
240 |
start = match.start() |
|
241 |
end = match.end() |
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
242 |
import_section = import_section[:start] + import_section[end:] |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
243 |
searchpos = start |
244 |
# Search for simple one-line import statements.
|
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
245 |
searchpos = 0 |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
246 |
while True: |
247 |
match = import_regex.search(import_section, searchpos) |
|
248 |
if match is None: |
|
249 |
break
|
|
250 |
# These imports are marked by a "None" value.
|
|
251 |
# Multiple modules in one statement are split up.
|
|
252 |
for module in split_regex.split(match.group('module').strip()): |
|
253 |
imports[module] = ImportStatement() |
|
254 |
searchpos = match.end() |
|
255 |
# Search for "from ... import" statements.
|
|
256 |
for pattern in (from_import_single_regex, from_import_multi_regex): |
|
257 |
searchpos = 0 |
|
258 |
while True: |
|
259 |
match = pattern.search(import_section, searchpos) |
|
260 |
if match is None: |
|
261 |
break
|
|
262 |
import_objects = split_regex.split( |
|
263 |
match.group('objects').strip(" \n,")) |
|
264 |
module = match.group('module').strip() |
|
265 |
# Only one pattern has a 'comment' group.
|
|
266 |
comment = match.groupdict().get('comment', None) |
|
267 |
if module in imports: |
|
268 |
# Catch double import lines.
|
|
269 |
imports[module].addObjects(import_objects) |
|
270 |
else: |
|
271 |
imports[module] = ImportStatement(import_objects) |
|
272 |
if comment is not None: |
|
273 |
imports[module].setComment(comment) |
|
274 |
searchpos = match.end() |
|
275 |
# Search for comments in import section.
|
|
276 |
searchpos = 0 |
|
277 |
while True: |
|
278 |
match = comment_regex.search(import_section, searchpos) |
|
279 |
if match is None: |
|
280 |
break
|
|
281 |
module = match.group('module').strip() |
|
282 |
comment = match.group('comment').strip() |
|
283 |
imports[module].setComment(comment) |
|
284 |
searchpos = match.end() |
|
285 |
||
286 |
return imports |
|
287 |
||
288 |
||
289 |
def format_imports(imports): |
|
290 |
"""Group and order imports, return the new import statements."""
|
|
291 |
standard_section = {} |
|
292 |
first_section = {} |
|
293 |
thirdparty_section = {} |
|
294 |
local_section = {} |
|
295 |
# Group modules into sections.
|
|
296 |
for module, statement in imports.iteritems(): |
|
297 |
module_base = module.split('.')[0] |
|
298 |
comment = statement.comment |
|
299 |
if comment is not None and comment.startswith("# FIRST"): |
|
300 |
first_section[module] = statement |
|
301 |
elif module_base in ('canonical', 'lp'): |
|
302 |
local_section[module] = statement |
|
303 |
elif module_base in python_standard_libs: |
|
304 |
standard_section[module] = statement |
|
305 |
else: |
|
306 |
thirdparty_section[module] = statement |
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
307 |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
308 |
all_import_lines = [] |
309 |
# Sort within each section and generate statement strings.
|
|
310 |
sections = ( |
|
311 |
standard_section, |
|
312 |
first_section, |
|
313 |
thirdparty_section, |
|
314 |
local_section, |
|
315 |
)
|
|
316 |
for section in sections: |
|
317 |
import_lines = [] |
|
318 |
for module in sorted(section.keys(), key=str.lower): |
|
319 |
if section[module].comment is not None: |
|
320 |
import_lines.append(section[module].comment) |
|
321 |
if section[module].import_module: |
|
322 |
import_lines.append("import %s" % module) |
|
323 |
if section[module].objects is not None: |
|
324 |
import_lines.append( |
|
325 |
format_import_lines(module, section[module].objects)) |
|
326 |
if len(import_lines) > 0: |
|
327 |
all_import_lines.append('\n'.join(import_lines)) |
|
11461.2.4
by Henning Eggers
Reviewer comments. |
328 |
# Sections are separated by two blank lines.
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
329 |
return '\n\n'.join(all_import_lines) |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
330 |
|
331 |
||
332 |
def reformat_importsection(filename): |
|
11461.2.4
by Henning Eggers
Reviewer comments. |
333 |
"""Replace the given file with a reformatted version of it."""
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
334 |
pyfile = file(filename).read() |
335 |
import_start, import_end = find_imports_section(pyfile) |
|
336 |
if import_start is None: |
|
337 |
# Skip files with no import section.
|
|
338 |
return False |
|
339 |
imports_section = pyfile[import_start:import_end] |
|
340 |
imports = parse_import_statements(imports_section) |
|
341 |
||
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
342 |
if pyfile[import_end:import_end + 1] != '#': |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
343 |
# Two newlines before anything but comments.
|
344 |
number_of_newlines = 3 |
|
345 |
else: |
|
346 |
number_of_newlines = 2 |
|
347 |
||
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
348 |
new_imports = format_imports(imports) + ("\n" * number_of_newlines) |
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
349 |
if new_imports == imports_section: |
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
350 |
# No change, no need to write a new file.
|
351 |
return False |
|
352 |
||
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
353 |
new_file = open(filename, "w") |
354 |
new_file.write(pyfile[:import_start]) |
|
355 |
new_file.write(new_imports) |
|
356 |
new_file.write(pyfile[import_end:]) |
|
357 |
||
358 |
return True |
|
359 |
||
360 |
||
361 |
def process_file(fpath): |
|
362 |
"""Process the file with the given path."""
|
|
363 |
changed = reformat_importsection(fpath) |
|
364 |
if changed: |
|
365 |
print fpath |
|
366 |
||
367 |
||
368 |
def process_tree(dpath): |
|
369 |
"""Walk a directory tree and process all *.py files."""
|
|
370 |
for dirpath, dirnames, filenames in os.walk(dpath): |
|
371 |
for filename in filenames: |
|
372 |
if filename.endswith('.py'): |
|
373 |
process_file(os.path.join(dirpath, filename)) |
|
374 |
||
375 |
||
376 |
if __name__ == "__main__": |
|
11461.2.2
by Henning Eggers
Made documentation easily available. |
377 |
if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"): |
378 |
sys.stderr.write(dedent("""\ |
|
379 |
usage: format-imports <file or directory> ...
|
|
11896.1.2
by Gavin Panella
Fix lint in utilities/format-imports. |
380 |
|
11461.2.2
by Henning Eggers
Made documentation easily available. |
381 |
Type "format-imports --docstring | less" to see the documentation.
|
382 |
""")) |
|
383 |
sys.exit(1) |
|
384 |
if sys.argv[1] == "--docstring": |
|
385 |
sys.stdout.write(__doc__) |
|
386 |
sys.exit(2) |
|
11461.2.1
by Henning Eggers
Added format-imports script and documented it. |
387 |
for filename in sys.argv[1:]: |
388 |
if os.path.isdir(filename): |
|
389 |
process_tree(filename) |
|
390 |
else: |
|
391 |
process_file(filename) |
|
11461.2.2
by Henning Eggers
Made documentation easily available. |
392 |
sys.exit(0) |