##// END OF EJS Templates
import-checker: recurse into subtree of sys.path only if __init__.py exists...
Yuya Nishihara -
r25733:f99c066f default
parent child Browse files
Show More
@@ -1,570 +1,571 b''
1 import ast
1 import ast
2 import os
2 import os
3 import sys
3 import sys
4
4
5 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
5 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
6 # to work when run from a virtualenv. The modules were chosen empirically
6 # to work when run from a virtualenv. The modules were chosen empirically
7 # so that the return value matches the return value without virtualenv.
7 # so that the return value matches the return value without virtualenv.
8 import BaseHTTPServer
8 import BaseHTTPServer
9 import zlib
9 import zlib
10
10
11 # Whitelist of modules that symbols can be directly imported from.
11 # Whitelist of modules that symbols can be directly imported from.
12 allowsymbolimports = (
12 allowsymbolimports = (
13 '__future__',
13 '__future__',
14 'mercurial.i18n',
14 'mercurial.i18n',
15 'mercurial.node',
15 'mercurial.node',
16 )
16 )
17
17
18 # Modules that must be aliased because they are commonly confused with
18 # Modules that must be aliased because they are commonly confused with
19 # common variables and can create aliasing and readability issues.
19 # common variables and can create aliasing and readability issues.
20 requirealias = {
20 requirealias = {
21 'ui': 'uimod',
21 'ui': 'uimod',
22 }
22 }
23
23
24 def usingabsolute(root):
24 def usingabsolute(root):
25 """Whether absolute imports are being used."""
25 """Whether absolute imports are being used."""
26 if sys.version_info[0] >= 3:
26 if sys.version_info[0] >= 3:
27 return True
27 return True
28
28
29 for node in ast.walk(root):
29 for node in ast.walk(root):
30 if isinstance(node, ast.ImportFrom):
30 if isinstance(node, ast.ImportFrom):
31 if node.module == '__future__':
31 if node.module == '__future__':
32 for n in node.names:
32 for n in node.names:
33 if n.name == 'absolute_import':
33 if n.name == 'absolute_import':
34 return True
34 return True
35
35
36 return False
36 return False
37
37
38 def dotted_name_of_path(path, trimpure=False):
38 def dotted_name_of_path(path, trimpure=False):
39 """Given a relative path to a source file, return its dotted module name.
39 """Given a relative path to a source file, return its dotted module name.
40
40
41 >>> dotted_name_of_path('mercurial/error.py')
41 >>> dotted_name_of_path('mercurial/error.py')
42 'mercurial.error'
42 'mercurial.error'
43 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
43 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
44 'mercurial.parsers'
44 'mercurial.parsers'
45 >>> dotted_name_of_path('zlibmodule.so')
45 >>> dotted_name_of_path('zlibmodule.so')
46 'zlib'
46 'zlib'
47 """
47 """
48 parts = path.split('/')
48 parts = path.split('/')
49 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
49 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
50 if parts[-1].endswith('module'):
50 if parts[-1].endswith('module'):
51 parts[-1] = parts[-1][:-6]
51 parts[-1] = parts[-1][:-6]
52 if trimpure:
52 if trimpure:
53 return '.'.join(p for p in parts if p != 'pure')
53 return '.'.join(p for p in parts if p != 'pure')
54 return '.'.join(parts)
54 return '.'.join(parts)
55
55
56 def fromlocalfunc(modulename, localmods):
56 def fromlocalfunc(modulename, localmods):
57 """Get a function to examine which locally defined module the
57 """Get a function to examine which locally defined module the
58 target source imports via a specified name.
58 target source imports via a specified name.
59
59
60 `modulename` is an `dotted_name_of_path()`-ed source file path,
60 `modulename` is an `dotted_name_of_path()`-ed source file path,
61 which may have `.__init__` at the end of it, of the target source.
61 which may have `.__init__` at the end of it, of the target source.
62
62
63 `localmods` is a dict (or set), of which key is an absolute
63 `localmods` is a dict (or set), of which key is an absolute
64 `dotted_name_of_path()`-ed source file path of locally defined (=
64 `dotted_name_of_path()`-ed source file path of locally defined (=
65 Mercurial specific) modules.
65 Mercurial specific) modules.
66
66
67 This function assumes that module names not existing in
67 This function assumes that module names not existing in
68 `localmods` are ones of Python standard libarary.
68 `localmods` are ones of Python standard libarary.
69
69
70 This function returns the function, which takes `name` argument,
70 This function returns the function, which takes `name` argument,
71 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
71 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
72 matches against locally defined module. Otherwise, it returns
72 matches against locally defined module. Otherwise, it returns
73 False.
73 False.
74
74
75 It is assumed that `name` doesn't have `.__init__`.
75 It is assumed that `name` doesn't have `.__init__`.
76
76
77 `absname` is an absolute module name of specified `name`
77 `absname` is an absolute module name of specified `name`
78 (e.g. "hgext.convert"). This can be used to compose prefix for sub
78 (e.g. "hgext.convert"). This can be used to compose prefix for sub
79 modules or so.
79 modules or so.
80
80
81 `dottedpath` is a `dotted_name_of_path()`-ed source file path
81 `dottedpath` is a `dotted_name_of_path()`-ed source file path
82 (e.g. "hgext.convert.__init__") of `name`. This is used to look
82 (e.g. "hgext.convert.__init__") of `name`. This is used to look
83 module up in `localmods` again.
83 module up in `localmods` again.
84
84
85 `hassubmod` is whether it may have sub modules under it (for
85 `hassubmod` is whether it may have sub modules under it (for
86 convenient, even though this is also equivalent to "absname !=
86 convenient, even though this is also equivalent to "absname !=
87 dottednpath")
87 dottednpath")
88
88
89 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
89 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
90 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
90 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
91 ... 'baz.__init__': True, 'baz.baz1': True }
91 ... 'baz.__init__': True, 'baz.baz1': True }
92 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
92 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
93 >>> # relative
93 >>> # relative
94 >>> fromlocal('foo1')
94 >>> fromlocal('foo1')
95 ('foo.foo1', 'foo.foo1', False)
95 ('foo.foo1', 'foo.foo1', False)
96 >>> fromlocal('bar')
96 >>> fromlocal('bar')
97 ('foo.bar', 'foo.bar.__init__', True)
97 ('foo.bar', 'foo.bar.__init__', True)
98 >>> fromlocal('bar.bar1')
98 >>> fromlocal('bar.bar1')
99 ('foo.bar.bar1', 'foo.bar.bar1', False)
99 ('foo.bar.bar1', 'foo.bar.bar1', False)
100 >>> # absolute
100 >>> # absolute
101 >>> fromlocal('baz')
101 >>> fromlocal('baz')
102 ('baz', 'baz.__init__', True)
102 ('baz', 'baz.__init__', True)
103 >>> fromlocal('baz.baz1')
103 >>> fromlocal('baz.baz1')
104 ('baz.baz1', 'baz.baz1', False)
104 ('baz.baz1', 'baz.baz1', False)
105 >>> # unknown = maybe standard library
105 >>> # unknown = maybe standard library
106 >>> fromlocal('os')
106 >>> fromlocal('os')
107 False
107 False
108 >>> fromlocal(None, 1)
108 >>> fromlocal(None, 1)
109 ('foo', 'foo.__init__', True)
109 ('foo', 'foo.__init__', True)
110 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
110 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
111 >>> fromlocal2(None, 2)
111 >>> fromlocal2(None, 2)
112 ('foo', 'foo.__init__', True)
112 ('foo', 'foo.__init__', True)
113 """
113 """
114 prefix = '.'.join(modulename.split('.')[:-1])
114 prefix = '.'.join(modulename.split('.')[:-1])
115 if prefix:
115 if prefix:
116 prefix += '.'
116 prefix += '.'
117 def fromlocal(name, level=0):
117 def fromlocal(name, level=0):
118 # name is None when relative imports are used.
118 # name is None when relative imports are used.
119 if name is None:
119 if name is None:
120 # If relative imports are used, level must not be absolute.
120 # If relative imports are used, level must not be absolute.
121 assert level > 0
121 assert level > 0
122 candidates = ['.'.join(modulename.split('.')[:-level])]
122 candidates = ['.'.join(modulename.split('.')[:-level])]
123 else:
123 else:
124 # Check relative name first.
124 # Check relative name first.
125 candidates = [prefix + name, name]
125 candidates = [prefix + name, name]
126
126
127 for n in candidates:
127 for n in candidates:
128 if n in localmods:
128 if n in localmods:
129 return (n, n, False)
129 return (n, n, False)
130 dottedpath = n + '.__init__'
130 dottedpath = n + '.__init__'
131 if dottedpath in localmods:
131 if dottedpath in localmods:
132 return (n, dottedpath, True)
132 return (n, dottedpath, True)
133 return False
133 return False
134 return fromlocal
134 return fromlocal
135
135
136 def list_stdlib_modules():
136 def list_stdlib_modules():
137 """List the modules present in the stdlib.
137 """List the modules present in the stdlib.
138
138
139 >>> mods = set(list_stdlib_modules())
139 >>> mods = set(list_stdlib_modules())
140 >>> 'BaseHTTPServer' in mods
140 >>> 'BaseHTTPServer' in mods
141 True
141 True
142
142
143 os.path isn't really a module, so it's missing:
143 os.path isn't really a module, so it's missing:
144
144
145 >>> 'os.path' in mods
145 >>> 'os.path' in mods
146 False
146 False
147
147
148 sys requires special treatment, because it's baked into the
148 sys requires special treatment, because it's baked into the
149 interpreter, but it should still appear:
149 interpreter, but it should still appear:
150
150
151 >>> 'sys' in mods
151 >>> 'sys' in mods
152 True
152 True
153
153
154 >>> 'collections' in mods
154 >>> 'collections' in mods
155 True
155 True
156
156
157 >>> 'cStringIO' in mods
157 >>> 'cStringIO' in mods
158 True
158 True
159 """
159 """
160 for m in sys.builtin_module_names:
160 for m in sys.builtin_module_names:
161 yield m
161 yield m
162 # These modules only exist on windows, but we should always
162 # These modules only exist on windows, but we should always
163 # consider them stdlib.
163 # consider them stdlib.
164 for m in ['msvcrt', '_winreg']:
164 for m in ['msvcrt', '_winreg']:
165 yield m
165 yield m
166 # These get missed too
166 # These get missed too
167 for m in 'ctypes', 'email':
167 for m in 'ctypes', 'email':
168 yield m
168 yield m
169 yield 'builtins' # python3 only
169 yield 'builtins' # python3 only
170 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
170 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
171 yield m
171 yield m
172 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
172 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
173 # We need to supplement the list of prefixes for the search to work
173 # We need to supplement the list of prefixes for the search to work
174 # when run from within a virtualenv.
174 # when run from within a virtualenv.
175 for mod in (BaseHTTPServer, zlib):
175 for mod in (BaseHTTPServer, zlib):
176 try:
176 try:
177 # Not all module objects have a __file__ attribute.
177 # Not all module objects have a __file__ attribute.
178 filename = mod.__file__
178 filename = mod.__file__
179 except AttributeError:
179 except AttributeError:
180 continue
180 continue
181 dirname = os.path.dirname(filename)
181 dirname = os.path.dirname(filename)
182 for prefix in stdlib_prefixes:
182 for prefix in stdlib_prefixes:
183 if dirname.startswith(prefix):
183 if dirname.startswith(prefix):
184 # Then this directory is redundant.
184 # Then this directory is redundant.
185 break
185 break
186 else:
186 else:
187 stdlib_prefixes.add(dirname)
187 stdlib_prefixes.add(dirname)
188 for libpath in sys.path:
188 for libpath in sys.path:
189 # We want to walk everything in sys.path that starts with
189 # We want to walk everything in sys.path that starts with
190 # something in stdlib_prefixes. check-code suppressed because
190 # something in stdlib_prefixes. check-code suppressed because
191 # the ast module used by this script implies the availability
191 # the ast module used by this script implies the availability
192 # of any().
192 # of any().
193 if not any(libpath.startswith(p) for p in stdlib_prefixes): # no-py24
193 if not any(libpath.startswith(p) for p in stdlib_prefixes): # no-py24
194 continue
194 continue
195 if 'site-packages' in libpath:
195 if 'site-packages' in libpath:
196 continue
196 continue
197 for top, dirs, files in os.walk(libpath):
197 for top, dirs, files in os.walk(libpath):
198 for i, d in reversed(list(enumerate(dirs))):
199 if not os.path.exists(os.path.join(top, d, '__init__.py')):
200 del dirs[i]
198 for name in files:
201 for name in files:
199 if name == '__init__.py':
202 if name == '__init__.py':
200 continue
203 continue
201 if not (name.endswith('.py') or name.endswith('.so')
204 if not (name.endswith('.py') or name.endswith('.so')
202 or name.endswith('.pyd')):
205 or name.endswith('.pyd')):
203 continue
206 continue
204 full_path = os.path.join(top, name)
207 full_path = os.path.join(top, name)
205 if 'site-packages' in full_path:
206 continue
207 rel_path = full_path[len(libpath) + 1:]
208 rel_path = full_path[len(libpath) + 1:]
208 mod = dotted_name_of_path(rel_path)
209 mod = dotted_name_of_path(rel_path)
209 yield mod
210 yield mod
210
211
211 stdlib_modules = set(list_stdlib_modules())
212 stdlib_modules = set(list_stdlib_modules())
212
213
213 def imported_modules(source, modulename, localmods, ignore_nested=False):
214 def imported_modules(source, modulename, localmods, ignore_nested=False):
214 """Given the source of a file as a string, yield the names
215 """Given the source of a file as a string, yield the names
215 imported by that file.
216 imported by that file.
216
217
217 Args:
218 Args:
218 source: The python source to examine as a string.
219 source: The python source to examine as a string.
219 modulename: of specified python source (may have `__init__`)
220 modulename: of specified python source (may have `__init__`)
220 localmods: dict of locally defined module names (may have `__init__`)
221 localmods: dict of locally defined module names (may have `__init__`)
221 ignore_nested: If true, import statements that do not start in
222 ignore_nested: If true, import statements that do not start in
222 column zero will be ignored.
223 column zero will be ignored.
223
224
224 Returns:
225 Returns:
225 A list of absolute module names imported by the given source.
226 A list of absolute module names imported by the given source.
226
227
227 >>> modulename = 'foo.xxx'
228 >>> modulename = 'foo.xxx'
228 >>> localmods = {'foo.__init__': True,
229 >>> localmods = {'foo.__init__': True,
229 ... 'foo.foo1': True, 'foo.foo2': True,
230 ... 'foo.foo1': True, 'foo.foo2': True,
230 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
231 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
231 ... 'baz.__init__': True, 'baz.baz1': True }
232 ... 'baz.__init__': True, 'baz.baz1': True }
232 >>> # standard library (= not locally defined ones)
233 >>> # standard library (= not locally defined ones)
233 >>> sorted(imported_modules(
234 >>> sorted(imported_modules(
234 ... 'from stdlib1 import foo, bar; import stdlib2',
235 ... 'from stdlib1 import foo, bar; import stdlib2',
235 ... modulename, localmods))
236 ... modulename, localmods))
236 []
237 []
237 >>> # relative importing
238 >>> # relative importing
238 >>> sorted(imported_modules(
239 >>> sorted(imported_modules(
239 ... 'import foo1; from bar import bar1',
240 ... 'import foo1; from bar import bar1',
240 ... modulename, localmods))
241 ... modulename, localmods))
241 ['foo.bar.__init__', 'foo.bar.bar1', 'foo.foo1']
242 ['foo.bar.__init__', 'foo.bar.bar1', 'foo.foo1']
242 >>> sorted(imported_modules(
243 >>> sorted(imported_modules(
243 ... 'from bar.bar1 import name1, name2, name3',
244 ... 'from bar.bar1 import name1, name2, name3',
244 ... modulename, localmods))
245 ... modulename, localmods))
245 ['foo.bar.bar1']
246 ['foo.bar.bar1']
246 >>> # absolute importing
247 >>> # absolute importing
247 >>> sorted(imported_modules(
248 >>> sorted(imported_modules(
248 ... 'from baz import baz1, name1',
249 ... 'from baz import baz1, name1',
249 ... modulename, localmods))
250 ... modulename, localmods))
250 ['baz.__init__', 'baz.baz1']
251 ['baz.__init__', 'baz.baz1']
251 >>> # mixed importing, even though it shouldn't be recommended
252 >>> # mixed importing, even though it shouldn't be recommended
252 >>> sorted(imported_modules(
253 >>> sorted(imported_modules(
253 ... 'import stdlib, foo1, baz',
254 ... 'import stdlib, foo1, baz',
254 ... modulename, localmods))
255 ... modulename, localmods))
255 ['baz.__init__', 'foo.foo1']
256 ['baz.__init__', 'foo.foo1']
256 >>> # ignore_nested
257 >>> # ignore_nested
257 >>> sorted(imported_modules(
258 >>> sorted(imported_modules(
258 ... '''import foo
259 ... '''import foo
259 ... def wat():
260 ... def wat():
260 ... import bar
261 ... import bar
261 ... ''', modulename, localmods))
262 ... ''', modulename, localmods))
262 ['foo.__init__', 'foo.bar.__init__']
263 ['foo.__init__', 'foo.bar.__init__']
263 >>> sorted(imported_modules(
264 >>> sorted(imported_modules(
264 ... '''import foo
265 ... '''import foo
265 ... def wat():
266 ... def wat():
266 ... import bar
267 ... import bar
267 ... ''', modulename, localmods, ignore_nested=True))
268 ... ''', modulename, localmods, ignore_nested=True))
268 ['foo.__init__']
269 ['foo.__init__']
269 """
270 """
270 fromlocal = fromlocalfunc(modulename, localmods)
271 fromlocal = fromlocalfunc(modulename, localmods)
271 for node in ast.walk(ast.parse(source)):
272 for node in ast.walk(ast.parse(source)):
272 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
273 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
273 continue
274 continue
274 if isinstance(node, ast.Import):
275 if isinstance(node, ast.Import):
275 for n in node.names:
276 for n in node.names:
276 found = fromlocal(n.name)
277 found = fromlocal(n.name)
277 if not found:
278 if not found:
278 # this should import standard library
279 # this should import standard library
279 continue
280 continue
280 yield found[1]
281 yield found[1]
281 elif isinstance(node, ast.ImportFrom):
282 elif isinstance(node, ast.ImportFrom):
282 found = fromlocal(node.module, node.level)
283 found = fromlocal(node.module, node.level)
283 if not found:
284 if not found:
284 # this should import standard library
285 # this should import standard library
285 continue
286 continue
286
287
287 absname, dottedpath, hassubmod = found
288 absname, dottedpath, hassubmod = found
288 yield dottedpath
289 yield dottedpath
289 if not hassubmod:
290 if not hassubmod:
290 # examination of "node.names" should be redundant
291 # examination of "node.names" should be redundant
291 # e.g.: from mercurial.node import nullid, nullrev
292 # e.g.: from mercurial.node import nullid, nullrev
292 continue
293 continue
293
294
294 prefix = absname + '.'
295 prefix = absname + '.'
295 for n in node.names:
296 for n in node.names:
296 found = fromlocal(prefix + n.name)
297 found = fromlocal(prefix + n.name)
297 if not found:
298 if not found:
298 # this should be a function or a property of "node.module"
299 # this should be a function or a property of "node.module"
299 continue
300 continue
300 yield found[1]
301 yield found[1]
301
302
302 def verify_import_convention(module, source):
303 def verify_import_convention(module, source):
303 """Verify imports match our established coding convention.
304 """Verify imports match our established coding convention.
304
305
305 We have 2 conventions: legacy and modern. The modern convention is in
306 We have 2 conventions: legacy and modern. The modern convention is in
306 effect when using absolute imports.
307 effect when using absolute imports.
307
308
308 The legacy convention only looks for mixed imports. The modern convention
309 The legacy convention only looks for mixed imports. The modern convention
309 is much more thorough.
310 is much more thorough.
310 """
311 """
311 root = ast.parse(source)
312 root = ast.parse(source)
312 absolute = usingabsolute(root)
313 absolute = usingabsolute(root)
313
314
314 if absolute:
315 if absolute:
315 return verify_modern_convention(module, root)
316 return verify_modern_convention(module, root)
316 else:
317 else:
317 return verify_stdlib_on_own_line(root)
318 return verify_stdlib_on_own_line(root)
318
319
319 def verify_modern_convention(module, root):
320 def verify_modern_convention(module, root):
320 """Verify a file conforms to the modern import convention rules.
321 """Verify a file conforms to the modern import convention rules.
321
322
322 The rules of the modern convention are:
323 The rules of the modern convention are:
323
324
324 * Ordering is stdlib followed by local imports. Each group is lexically
325 * Ordering is stdlib followed by local imports. Each group is lexically
325 sorted.
326 sorted.
326 * Importing multiple modules via "import X, Y" is not allowed: use
327 * Importing multiple modules via "import X, Y" is not allowed: use
327 separate import statements.
328 separate import statements.
328 * Importing multiple modules via "from X import ..." is allowed if using
329 * Importing multiple modules via "from X import ..." is allowed if using
329 parenthesis and one entry per line.
330 parenthesis and one entry per line.
330 * Only 1 relative import statement per import level ("from .", "from ..")
331 * Only 1 relative import statement per import level ("from .", "from ..")
331 is allowed.
332 is allowed.
332 * Relative imports from higher levels must occur before lower levels. e.g.
333 * Relative imports from higher levels must occur before lower levels. e.g.
333 "from .." must be before "from .".
334 "from .." must be before "from .".
334 * Imports from peer packages should use relative import (e.g. do not
335 * Imports from peer packages should use relative import (e.g. do not
335 "import mercurial.foo" from a "mercurial.*" module).
336 "import mercurial.foo" from a "mercurial.*" module).
336 * Symbols can only be imported from specific modules (see
337 * Symbols can only be imported from specific modules (see
337 `allowsymbolimports`). For other modules, first import the module then
338 `allowsymbolimports`). For other modules, first import the module then
338 assign the symbol to a module-level variable. In addition, these imports
339 assign the symbol to a module-level variable. In addition, these imports
339 must be performed before other relative imports. This rule only
340 must be performed before other relative imports. This rule only
340 applies to import statements outside of any blocks.
341 applies to import statements outside of any blocks.
341 * Relative imports from the standard library are not allowed.
342 * Relative imports from the standard library are not allowed.
342 * Certain modules must be aliased to alternate names to avoid aliasing
343 * Certain modules must be aliased to alternate names to avoid aliasing
343 and readability problems. See `requirealias`.
344 and readability problems. See `requirealias`.
344 """
345 """
345 topmodule = module.split('.')[0]
346 topmodule = module.split('.')[0]
346
347
347 # Whether a local/non-stdlib import has been performed.
348 # Whether a local/non-stdlib import has been performed.
348 seenlocal = False
349 seenlocal = False
349 # Whether a relative, non-symbol import has been seen.
350 # Whether a relative, non-symbol import has been seen.
350 seennonsymbolrelative = False
351 seennonsymbolrelative = False
351 # The last name to be imported (for sorting).
352 # The last name to be imported (for sorting).
352 lastname = None
353 lastname = None
353 # Relative import levels encountered so far.
354 # Relative import levels encountered so far.
354 seenlevels = set()
355 seenlevels = set()
355
356
356 for node in ast.walk(root):
357 for node in ast.walk(root):
357 if isinstance(node, ast.Import):
358 if isinstance(node, ast.Import):
358 # Disallow "import foo, bar" and require separate imports
359 # Disallow "import foo, bar" and require separate imports
359 # for each module.
360 # for each module.
360 if len(node.names) > 1:
361 if len(node.names) > 1:
361 yield 'multiple imported names: %s' % ', '.join(
362 yield 'multiple imported names: %s' % ', '.join(
362 n.name for n in node.names)
363 n.name for n in node.names)
363
364
364 name = node.names[0].name
365 name = node.names[0].name
365 asname = node.names[0].asname
366 asname = node.names[0].asname
366
367
367 # Ignore sorting rules on imports inside blocks.
368 # Ignore sorting rules on imports inside blocks.
368 if node.col_offset == 0:
369 if node.col_offset == 0:
369 if lastname and name < lastname:
370 if lastname and name < lastname:
370 yield 'imports not lexically sorted: %s < %s' % (
371 yield 'imports not lexically sorted: %s < %s' % (
371 name, lastname)
372 name, lastname)
372
373
373 lastname = name
374 lastname = name
374
375
375 # stdlib imports should be before local imports.
376 # stdlib imports should be before local imports.
376 stdlib = name in stdlib_modules
377 stdlib = name in stdlib_modules
377 if stdlib and seenlocal and node.col_offset == 0:
378 if stdlib and seenlocal and node.col_offset == 0:
378 yield 'stdlib import follows local import: %s' % name
379 yield 'stdlib import follows local import: %s' % name
379
380
380 if not stdlib:
381 if not stdlib:
381 seenlocal = True
382 seenlocal = True
382
383
383 # Import of sibling modules should use relative imports.
384 # Import of sibling modules should use relative imports.
384 topname = name.split('.')[0]
385 topname = name.split('.')[0]
385 if topname == topmodule:
386 if topname == topmodule:
386 yield 'import should be relative: %s' % name
387 yield 'import should be relative: %s' % name
387
388
388 if name in requirealias and asname != requirealias[name]:
389 if name in requirealias and asname != requirealias[name]:
389 yield '%s module must be "as" aliased to %s' % (
390 yield '%s module must be "as" aliased to %s' % (
390 name, requirealias[name])
391 name, requirealias[name])
391
392
392 elif isinstance(node, ast.ImportFrom):
393 elif isinstance(node, ast.ImportFrom):
393 # Resolve the full imported module name.
394 # Resolve the full imported module name.
394 if node.level > 0:
395 if node.level > 0:
395 fullname = '.'.join(module.split('.')[:-node.level])
396 fullname = '.'.join(module.split('.')[:-node.level])
396 if node.module:
397 if node.module:
397 fullname += '.%s' % node.module
398 fullname += '.%s' % node.module
398 else:
399 else:
399 assert node.module
400 assert node.module
400 fullname = node.module
401 fullname = node.module
401
402
402 topname = fullname.split('.')[0]
403 topname = fullname.split('.')[0]
403 if topname == topmodule:
404 if topname == topmodule:
404 yield 'import should be relative: %s' % fullname
405 yield 'import should be relative: %s' % fullname
405
406
406 # __future__ is special since it needs to come first and use
407 # __future__ is special since it needs to come first and use
407 # symbol import.
408 # symbol import.
408 if fullname != '__future__':
409 if fullname != '__future__':
409 if not fullname or fullname in stdlib_modules:
410 if not fullname or fullname in stdlib_modules:
410 yield 'relative import of stdlib module'
411 yield 'relative import of stdlib module'
411 else:
412 else:
412 seenlocal = True
413 seenlocal = True
413
414
414 # Direct symbol import is only allowed from certain modules and
415 # Direct symbol import is only allowed from certain modules and
415 # must occur before non-symbol imports.
416 # must occur before non-symbol imports.
416 if node.module and node.col_offset == 0:
417 if node.module and node.col_offset == 0:
417 if fullname not in allowsymbolimports:
418 if fullname not in allowsymbolimports:
418 yield 'direct symbol import from %s' % fullname
419 yield 'direct symbol import from %s' % fullname
419
420
420 if seennonsymbolrelative:
421 if seennonsymbolrelative:
421 yield ('symbol import follows non-symbol import: %s' %
422 yield ('symbol import follows non-symbol import: %s' %
422 fullname)
423 fullname)
423
424
424 if not node.module:
425 if not node.module:
425 assert node.level
426 assert node.level
426 seennonsymbolrelative = True
427 seennonsymbolrelative = True
427
428
428 # Only allow 1 group per level.
429 # Only allow 1 group per level.
429 if node.level in seenlevels and node.col_offset == 0:
430 if node.level in seenlevels and node.col_offset == 0:
430 yield 'multiple "from %s import" statements' % (
431 yield 'multiple "from %s import" statements' % (
431 '.' * node.level)
432 '.' * node.level)
432
433
433 # Higher-level groups come before lower-level groups.
434 # Higher-level groups come before lower-level groups.
434 if any(node.level > l for l in seenlevels):
435 if any(node.level > l for l in seenlevels):
435 yield 'higher-level import should come first: %s' % (
436 yield 'higher-level import should come first: %s' % (
436 fullname)
437 fullname)
437
438
438 seenlevels.add(node.level)
439 seenlevels.add(node.level)
439
440
440 # Entries in "from .X import ( ... )" lists must be lexically
441 # Entries in "from .X import ( ... )" lists must be lexically
441 # sorted.
442 # sorted.
442 lastentryname = None
443 lastentryname = None
443
444
444 for n in node.names:
445 for n in node.names:
445 if lastentryname and n.name < lastentryname:
446 if lastentryname and n.name < lastentryname:
446 yield 'imports from %s not lexically sorted: %s < %s' % (
447 yield 'imports from %s not lexically sorted: %s < %s' % (
447 fullname, n.name, lastentryname)
448 fullname, n.name, lastentryname)
448
449
449 lastentryname = n.name
450 lastentryname = n.name
450
451
451 if n.name in requirealias and n.asname != requirealias[n.name]:
452 if n.name in requirealias and n.asname != requirealias[n.name]:
452 yield '%s from %s must be "as" aliased to %s' % (
453 yield '%s from %s must be "as" aliased to %s' % (
453 n.name, fullname, requirealias[n.name])
454 n.name, fullname, requirealias[n.name])
454
455
455 def verify_stdlib_on_own_line(root):
456 def verify_stdlib_on_own_line(root):
456 """Given some python source, verify that stdlib imports are done
457 """Given some python source, verify that stdlib imports are done
457 in separate statements from relative local module imports.
458 in separate statements from relative local module imports.
458
459
459 Observing this limitation is important as it works around an
460 Observing this limitation is important as it works around an
460 annoying lib2to3 bug in relative import rewrites:
461 annoying lib2to3 bug in relative import rewrites:
461 http://bugs.python.org/issue19510.
462 http://bugs.python.org/issue19510.
462
463
463 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
464 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
464 ['mixed imports\\n stdlib: sys\\n relative: foo']
465 ['mixed imports\\n stdlib: sys\\n relative: foo']
465 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
466 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
466 []
467 []
467 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
468 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
468 []
469 []
469 """
470 """
470 for node in ast.walk(root):
471 for node in ast.walk(root):
471 if isinstance(node, ast.Import):
472 if isinstance(node, ast.Import):
472 from_stdlib = {False: [], True: []}
473 from_stdlib = {False: [], True: []}
473 for n in node.names:
474 for n in node.names:
474 from_stdlib[n.name in stdlib_modules].append(n.name)
475 from_stdlib[n.name in stdlib_modules].append(n.name)
475 if from_stdlib[True] and from_stdlib[False]:
476 if from_stdlib[True] and from_stdlib[False]:
476 yield ('mixed imports\n stdlib: %s\n relative: %s' %
477 yield ('mixed imports\n stdlib: %s\n relative: %s' %
477 (', '.join(sorted(from_stdlib[True])),
478 (', '.join(sorted(from_stdlib[True])),
478 ', '.join(sorted(from_stdlib[False]))))
479 ', '.join(sorted(from_stdlib[False]))))
479
480
480 class CircularImport(Exception):
481 class CircularImport(Exception):
481 pass
482 pass
482
483
483 def checkmod(mod, imports):
484 def checkmod(mod, imports):
484 shortest = {}
485 shortest = {}
485 visit = [[mod]]
486 visit = [[mod]]
486 while visit:
487 while visit:
487 path = visit.pop(0)
488 path = visit.pop(0)
488 for i in sorted(imports.get(path[-1], [])):
489 for i in sorted(imports.get(path[-1], [])):
489 if len(path) < shortest.get(i, 1000):
490 if len(path) < shortest.get(i, 1000):
490 shortest[i] = len(path)
491 shortest[i] = len(path)
491 if i in path:
492 if i in path:
492 if i == path[0]:
493 if i == path[0]:
493 raise CircularImport(path)
494 raise CircularImport(path)
494 continue
495 continue
495 visit.append(path + [i])
496 visit.append(path + [i])
496
497
497 def rotatecycle(cycle):
498 def rotatecycle(cycle):
498 """arrange a cycle so that the lexicographically first module listed first
499 """arrange a cycle so that the lexicographically first module listed first
499
500
500 >>> rotatecycle(['foo', 'bar'])
501 >>> rotatecycle(['foo', 'bar'])
501 ['bar', 'foo', 'bar']
502 ['bar', 'foo', 'bar']
502 """
503 """
503 lowest = min(cycle)
504 lowest = min(cycle)
504 idx = cycle.index(lowest)
505 idx = cycle.index(lowest)
505 return cycle[idx:] + cycle[:idx] + [lowest]
506 return cycle[idx:] + cycle[:idx] + [lowest]
506
507
507 def find_cycles(imports):
508 def find_cycles(imports):
508 """Find cycles in an already-loaded import graph.
509 """Find cycles in an already-loaded import graph.
509
510
510 All module names recorded in `imports` should be absolute one.
511 All module names recorded in `imports` should be absolute one.
511
512
512 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
513 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
513 ... 'top.bar': ['top.baz', 'sys'],
514 ... 'top.bar': ['top.baz', 'sys'],
514 ... 'top.baz': ['top.foo'],
515 ... 'top.baz': ['top.foo'],
515 ... 'top.qux': ['top.foo']}
516 ... 'top.qux': ['top.foo']}
516 >>> print '\\n'.join(sorted(find_cycles(imports)))
517 >>> print '\\n'.join(sorted(find_cycles(imports)))
517 top.bar -> top.baz -> top.foo -> top.bar
518 top.bar -> top.baz -> top.foo -> top.bar
518 top.foo -> top.qux -> top.foo
519 top.foo -> top.qux -> top.foo
519 """
520 """
520 cycles = set()
521 cycles = set()
521 for mod in sorted(imports.iterkeys()):
522 for mod in sorted(imports.iterkeys()):
522 try:
523 try:
523 checkmod(mod, imports)
524 checkmod(mod, imports)
524 except CircularImport as e:
525 except CircularImport as e:
525 cycle = e.args[0]
526 cycle = e.args[0]
526 cycles.add(" -> ".join(rotatecycle(cycle)))
527 cycles.add(" -> ".join(rotatecycle(cycle)))
527 return cycles
528 return cycles
528
529
529 def _cycle_sortkey(c):
530 def _cycle_sortkey(c):
530 return len(c), c
531 return len(c), c
531
532
532 def main(argv):
533 def main(argv):
533 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
534 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
534 print 'Usage: %s {-|file [file] [file] ...}'
535 print 'Usage: %s {-|file [file] [file] ...}'
535 return 1
536 return 1
536 if argv[1] == '-':
537 if argv[1] == '-':
537 argv = argv[:1]
538 argv = argv[:1]
538 argv.extend(l.rstrip() for l in sys.stdin.readlines())
539 argv.extend(l.rstrip() for l in sys.stdin.readlines())
539 localmods = {}
540 localmods = {}
540 used_imports = {}
541 used_imports = {}
541 any_errors = False
542 any_errors = False
542 for source_path in argv[1:]:
543 for source_path in argv[1:]:
543 modname = dotted_name_of_path(source_path, trimpure=True)
544 modname = dotted_name_of_path(source_path, trimpure=True)
544 localmods[modname] = source_path
545 localmods[modname] = source_path
545 for modname, source_path in sorted(localmods.iteritems()):
546 for modname, source_path in sorted(localmods.iteritems()):
546 f = open(source_path)
547 f = open(source_path)
547 src = f.read()
548 src = f.read()
548 used_imports[modname] = sorted(
549 used_imports[modname] = sorted(
549 imported_modules(src, modname, localmods, ignore_nested=True))
550 imported_modules(src, modname, localmods, ignore_nested=True))
550 for error in verify_import_convention(modname, src):
551 for error in verify_import_convention(modname, src):
551 any_errors = True
552 any_errors = True
552 print source_path, error
553 print source_path, error
553 f.close()
554 f.close()
554 cycles = find_cycles(used_imports)
555 cycles = find_cycles(used_imports)
555 if cycles:
556 if cycles:
556 firstmods = set()
557 firstmods = set()
557 for c in sorted(cycles, key=_cycle_sortkey):
558 for c in sorted(cycles, key=_cycle_sortkey):
558 first = c.split()[0]
559 first = c.split()[0]
559 # As a rough cut, ignore any cycle that starts with the
560 # As a rough cut, ignore any cycle that starts with the
560 # same module as some other cycle. Otherwise we see lots
561 # same module as some other cycle. Otherwise we see lots
561 # of cycles that are effectively duplicates.
562 # of cycles that are effectively duplicates.
562 if first in firstmods:
563 if first in firstmods:
563 continue
564 continue
564 print 'Import cycle:', c
565 print 'Import cycle:', c
565 firstmods.add(first)
566 firstmods.add(first)
566 any_errors = True
567 any_errors = True
567 return any_errors != 0
568 return any_errors != 0
568
569
569 if __name__ == '__main__':
570 if __name__ == '__main__':
570 sys.exit(int(main(sys.argv)))
571 sys.exit(int(main(sys.argv)))
General Comments 0
You need to be logged in to leave comments. Login now