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