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