##// END OF EJS Templates
import-checker: don't treat modules as relative one if not found...
FUJIWARA Katsunori -
r25175:10e6c4b7 default
parent child Browse files
Show More
@@ -1,377 +1,377 b''
1 1 import ast
2 2 import os
3 3 import sys
4 4
5 5 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
6 6 # to work when run from a virtualenv. The modules were chosen empirically
7 7 # so that the return value matches the return value without virtualenv.
8 8 import BaseHTTPServer
9 9 import zlib
10 10
11 11 def dotted_name_of_path(path, trimpure=False):
12 12 """Given a relative path to a source file, return its dotted module name.
13 13
14 14 >>> dotted_name_of_path('mercurial/error.py')
15 15 'mercurial.error'
16 16 >>> dotted_name_of_path('mercurial/pure/parsers.py', trimpure=True)
17 17 'mercurial.parsers'
18 18 >>> dotted_name_of_path('zlibmodule.so')
19 19 'zlib'
20 20 """
21 21 parts = path.split('/')
22 22 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
23 23 if parts[-1].endswith('module'):
24 24 parts[-1] = parts[-1][:-6]
25 25 if trimpure:
26 26 return '.'.join(p for p in parts if p != 'pure')
27 27 return '.'.join(parts)
28 28
29 29 def fromlocalfunc(modulename, localmods):
30 30 """Get a function to examine which locally defined module the
31 31 target source imports via a specified name.
32 32
33 33 `modulename` is an `dotted_name_of_path()`-ed source file path,
34 34 which may have `.__init__` at the end of it, of the target source.
35 35
36 36 `localmods` is a dict (or set), of which key is an absolute
37 37 `dotted_name_of_path()`-ed source file path of locally defined (=
38 38 Mercurial specific) modules.
39 39
40 40 This function assumes that module names not existing in
41 41 `localmods` are ones of Python standard libarary.
42 42
43 43 This function returns the function, which takes `name` argument,
44 44 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
45 45 matches against locally defined module. Otherwise, it returns
46 46 False.
47 47
48 48 It is assumed that `name` doesn't have `.__init__`.
49 49
50 50 `absname` is an absolute module name of specified `name`
51 51 (e.g. "hgext.convert"). This can be used to compose prefix for sub
52 52 modules or so.
53 53
54 54 `dottedpath` is a `dotted_name_of_path()`-ed source file path
55 55 (e.g. "hgext.convert.__init__") of `name`. This is used to look
56 56 module up in `localmods` again.
57 57
58 58 `hassubmod` is whether it may have sub modules under it (for
59 59 convenient, even though this is also equivalent to "absname !=
60 60 dottednpath")
61 61
62 62 >>> localmods = {'foo.__init__': True, 'foo.foo1': True,
63 63 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
64 64 ... 'baz.__init__': True, 'baz.baz1': True }
65 65 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
66 66 >>> # relative
67 67 >>> fromlocal('foo1')
68 68 ('foo.foo1', 'foo.foo1', False)
69 69 >>> fromlocal('bar')
70 70 ('foo.bar', 'foo.bar.__init__', True)
71 71 >>> fromlocal('bar.bar1')
72 72 ('foo.bar.bar1', 'foo.bar.bar1', False)
73 73 >>> # absolute
74 74 >>> fromlocal('baz')
75 75 ('baz', 'baz.__init__', True)
76 76 >>> fromlocal('baz.baz1')
77 77 ('baz.baz1', 'baz.baz1', False)
78 78 >>> # unknown = maybe standard library
79 79 >>> fromlocal('os')
80 80 False
81 81 """
82 82 prefix = '.'.join(modulename.split('.')[:-1])
83 83 if prefix:
84 84 prefix += '.'
85 85 def fromlocal(name):
86 86 # check relative name at first
87 87 for n in prefix + name, name:
88 88 if n in localmods:
89 89 return (n, n, False)
90 90 dottedpath = n + '.__init__'
91 91 if dottedpath in localmods:
92 92 return (n, dottedpath, True)
93 93 return False
94 94 return fromlocal
95 95
96 96 def list_stdlib_modules():
97 97 """List the modules present in the stdlib.
98 98
99 99 >>> mods = set(list_stdlib_modules())
100 100 >>> 'BaseHTTPServer' in mods
101 101 True
102 102
103 103 os.path isn't really a module, so it's missing:
104 104
105 105 >>> 'os.path' in mods
106 106 False
107 107
108 108 sys requires special treatment, because it's baked into the
109 109 interpreter, but it should still appear:
110 110
111 111 >>> 'sys' in mods
112 112 True
113 113
114 114 >>> 'collections' in mods
115 115 True
116 116
117 117 >>> 'cStringIO' in mods
118 118 True
119 119 """
120 120 for m in sys.builtin_module_names:
121 121 yield m
122 122 # These modules only exist on windows, but we should always
123 123 # consider them stdlib.
124 124 for m in ['msvcrt', '_winreg']:
125 125 yield m
126 126 # These get missed too
127 127 for m in 'ctypes', 'email':
128 128 yield m
129 129 yield 'builtins' # python3 only
130 130 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
131 131 yield m
132 132 stdlib_prefixes = set([sys.prefix, sys.exec_prefix])
133 133 # We need to supplement the list of prefixes for the search to work
134 134 # when run from within a virtualenv.
135 135 for mod in (BaseHTTPServer, zlib):
136 136 try:
137 137 # Not all module objects have a __file__ attribute.
138 138 filename = mod.__file__
139 139 except AttributeError:
140 140 continue
141 141 dirname = os.path.dirname(filename)
142 142 for prefix in stdlib_prefixes:
143 143 if dirname.startswith(prefix):
144 144 # Then this directory is redundant.
145 145 break
146 146 else:
147 147 stdlib_prefixes.add(dirname)
148 148 for libpath in sys.path:
149 149 # We want to walk everything in sys.path that starts with
150 150 # something in stdlib_prefixes. check-code suppressed because
151 151 # the ast module used by this script implies the availability
152 152 # of any().
153 153 if not any(libpath.startswith(p) for p in stdlib_prefixes): # no-py24
154 154 continue
155 155 if 'site-packages' in libpath:
156 156 continue
157 157 for top, dirs, files in os.walk(libpath):
158 158 for name in files:
159 159 if name == '__init__.py':
160 160 continue
161 161 if not (name.endswith('.py') or name.endswith('.so')
162 162 or name.endswith('.pyd')):
163 163 continue
164 164 full_path = os.path.join(top, name)
165 165 if 'site-packages' in full_path:
166 166 continue
167 167 rel_path = full_path[len(libpath) + 1:]
168 168 mod = dotted_name_of_path(rel_path)
169 169 yield mod
170 170
171 171 stdlib_modules = set(list_stdlib_modules())
172 172
173 173 def imported_modules(source, modulename, localmods, ignore_nested=False):
174 174 """Given the source of a file as a string, yield the names
175 175 imported by that file.
176 176
177 177 Args:
178 178 source: The python source to examine as a string.
179 179 modulename: of specified python source (may have `__init__`)
180 180 localmods: dict of locally defined module names (may have `__init__`)
181 181 ignore_nested: If true, import statements that do not start in
182 182 column zero will be ignored.
183 183
184 184 Returns:
185 185 A list of absolute module names imported by the given source.
186 186
187 187 >>> modulename = 'foo.xxx'
188 188 >>> localmods = {'foo.__init__': True,
189 189 ... 'foo.foo1': True, 'foo.foo2': True,
190 190 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
191 191 ... 'baz.__init__': True, 'baz.baz1': True }
192 192 >>> # standard library (= not locally defined ones)
193 193 >>> sorted(imported_modules(
194 194 ... 'from stdlib1 import foo, bar; import stdlib2',
195 195 ... modulename, localmods))
196 196 []
197 197 >>> # relative importing
198 198 >>> sorted(imported_modules(
199 199 ... 'import foo1; from bar import bar1',
200 200 ... modulename, localmods))
201 201 ['foo.bar.__init__', 'foo.bar.bar1', 'foo.foo1']
202 202 >>> sorted(imported_modules(
203 203 ... 'from bar.bar1 import name1, name2, name3',
204 204 ... modulename, localmods))
205 205 ['foo.bar.bar1']
206 206 >>> # absolute importing
207 207 >>> sorted(imported_modules(
208 208 ... 'from baz import baz1, name1',
209 209 ... modulename, localmods))
210 210 ['baz.__init__', 'baz.baz1']
211 211 >>> # mixed importing, even though it shouldn't be recommended
212 212 >>> sorted(imported_modules(
213 213 ... 'import stdlib, foo1, baz',
214 214 ... modulename, localmods))
215 215 ['baz.__init__', 'foo.foo1']
216 216 >>> # ignore_nested
217 217 >>> sorted(imported_modules(
218 218 ... '''import foo
219 219 ... def wat():
220 220 ... import bar
221 221 ... ''', modulename, localmods))
222 222 ['foo.__init__', 'foo.bar.__init__']
223 223 >>> sorted(imported_modules(
224 224 ... '''import foo
225 225 ... def wat():
226 226 ... import bar
227 227 ... ''', modulename, localmods, ignore_nested=True))
228 228 ['foo.__init__']
229 229 """
230 230 fromlocal = fromlocalfunc(modulename, localmods)
231 231 for node in ast.walk(ast.parse(source)):
232 232 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
233 233 continue
234 234 if isinstance(node, ast.Import):
235 235 for n in node.names:
236 236 found = fromlocal(n.name)
237 237 if not found:
238 238 # this should import standard library
239 239 continue
240 240 yield found[1]
241 241 elif isinstance(node, ast.ImportFrom):
242 242 found = fromlocal(node.module)
243 243 if not found:
244 244 # this should import standard library
245 245 continue
246 246
247 247 absname, dottedpath, hassubmod = found
248 248 yield dottedpath
249 249 if not hassubmod:
250 250 # examination of "node.names" should be redundant
251 251 # e.g.: from mercurial.node import nullid, nullrev
252 252 continue
253 253
254 254 prefix = absname + '.'
255 255 for n in node.names:
256 256 found = fromlocal(prefix + n.name)
257 257 if not found:
258 258 # this should be a function or a property of "node.module"
259 259 continue
260 260 yield found[1]
261 261
262 262 def verify_stdlib_on_own_line(source):
263 263 """Given some python source, verify that stdlib imports are done
264 264 in separate statements from relative local module imports.
265 265
266 266 Observing this limitation is important as it works around an
267 267 annoying lib2to3 bug in relative import rewrites:
268 268 http://bugs.python.org/issue19510.
269 269
270 270 >>> list(verify_stdlib_on_own_line('import sys, foo'))
271 271 ['mixed imports\\n stdlib: sys\\n relative: foo']
272 272 >>> list(verify_stdlib_on_own_line('import sys, os'))
273 273 []
274 274 >>> list(verify_stdlib_on_own_line('import foo, bar'))
275 275 []
276 276 """
277 277 for node in ast.walk(ast.parse(source)):
278 278 if isinstance(node, ast.Import):
279 279 from_stdlib = {False: [], True: []}
280 280 for n in node.names:
281 281 from_stdlib[n.name in stdlib_modules].append(n.name)
282 282 if from_stdlib[True] and from_stdlib[False]:
283 283 yield ('mixed imports\n stdlib: %s\n relative: %s' %
284 284 (', '.join(sorted(from_stdlib[True])),
285 285 ', '.join(sorted(from_stdlib[False]))))
286 286
287 287 class CircularImport(Exception):
288 288 pass
289 289
290 290 def checkmod(mod, imports):
291 291 shortest = {}
292 292 visit = [[mod]]
293 293 while visit:
294 294 path = visit.pop(0)
295 295 for i in sorted(imports.get(path[-1], [])):
296 if i not in stdlib_modules and not i.startswith('mercurial.'):
297 i = mod.rsplit('.', 1)[0] + '.' + i
298 296 if len(path) < shortest.get(i, 1000):
299 297 shortest[i] = len(path)
300 298 if i in path:
301 299 if i == path[0]:
302 300 raise CircularImport(path)
303 301 continue
304 302 visit.append(path + [i])
305 303
306 304 def rotatecycle(cycle):
307 305 """arrange a cycle so that the lexicographically first module listed first
308 306
309 307 >>> rotatecycle(['foo', 'bar'])
310 308 ['bar', 'foo', 'bar']
311 309 """
312 310 lowest = min(cycle)
313 311 idx = cycle.index(lowest)
314 312 return cycle[idx:] + cycle[:idx] + [lowest]
315 313
316 314 def find_cycles(imports):
317 315 """Find cycles in an already-loaded import graph.
318 316
319 >>> imports = {'top.foo': ['bar', 'os.path', 'qux'],
320 ... 'top.bar': ['baz', 'sys'],
321 ... 'top.baz': ['foo'],
322 ... 'top.qux': ['foo']}
317 All module names recorded in `imports` should be absolute one.
318
319 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
320 ... 'top.bar': ['top.baz', 'sys'],
321 ... 'top.baz': ['top.foo'],
322 ... 'top.qux': ['top.foo']}
323 323 >>> print '\\n'.join(sorted(find_cycles(imports)))
324 324 top.bar -> top.baz -> top.foo -> top.bar
325 325 top.foo -> top.qux -> top.foo
326 326 """
327 327 cycles = set()
328 328 for mod in sorted(imports.iterkeys()):
329 329 try:
330 330 checkmod(mod, imports)
331 331 except CircularImport, e:
332 332 cycle = e.args[0]
333 333 cycles.add(" -> ".join(rotatecycle(cycle)))
334 334 return cycles
335 335
336 336 def _cycle_sortkey(c):
337 337 return len(c), c
338 338
339 339 def main(argv):
340 340 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
341 341 print 'Usage: %s {-|file [file] [file] ...}'
342 342 return 1
343 343 if argv[1] == '-':
344 344 argv = argv[:1]
345 345 argv.extend(l.rstrip() for l in sys.stdin.readlines())
346 346 localmods = {}
347 347 used_imports = {}
348 348 any_errors = False
349 349 for source_path in argv[1:]:
350 350 modname = dotted_name_of_path(source_path, trimpure=True)
351 351 localmods[modname] = source_path
352 352 for modname, source_path in sorted(localmods.iteritems()):
353 353 f = open(source_path)
354 354 src = f.read()
355 355 used_imports[modname] = sorted(
356 356 imported_modules(src, modname, localmods, ignore_nested=True))
357 357 for error in verify_stdlib_on_own_line(src):
358 358 any_errors = True
359 359 print source_path, error
360 360 f.close()
361 361 cycles = find_cycles(used_imports)
362 362 if cycles:
363 363 firstmods = set()
364 364 for c in sorted(cycles, key=_cycle_sortkey):
365 365 first = c.split()[0]
366 366 # As a rough cut, ignore any cycle that starts with the
367 367 # same module as some other cycle. Otherwise we see lots
368 368 # of cycles that are effectively duplicates.
369 369 if first in firstmods:
370 370 continue
371 371 print 'Import cycle:', c
372 372 firstmods.add(first)
373 373 any_errors = True
374 374 return not any_errors
375 375
376 376 if __name__ == '__main__':
377 377 sys.exit(int(main(sys.argv)))
General Comments 0
You need to be logged in to leave comments. Login now