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