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