##// END OF EJS Templates
contrib: tweak import-checker to always use bytes for module names...
Augie Fackler -
r40515:90517fad default
parent child Browse files
Show More
@@ -1,747 +1,747
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 263 for libpath in sys.path:
264 264 # We want to walk everything in sys.path that starts with
265 265 # something in stdlib_prefixes.
266 266 if not any(libpath.startswith(p) for p in stdlib_prefixes):
267 267 continue
268 268 for top, dirs, files in os.walk(libpath):
269 269 for i, d in reversed(list(enumerate(dirs))):
270 270 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
271 271 or top == libpath and d in ('hgdemandimport', 'hgext',
272 272 'mercurial')):
273 273 del dirs[i]
274 274 for name in files:
275 275 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
276 276 continue
277 277 if name.startswith('__init__.py'):
278 278 full_path = top
279 279 else:
280 280 full_path = os.path.join(top, name)
281 281 rel_path = full_path[len(libpath) + 1:]
282 282 mod = dotted_name_of_path(rel_path)
283 283 yield mod
284 284
285 285 stdlib_modules = set(list_stdlib_modules())
286 286
287 287 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
288 288 """Given the source of a file as a string, yield the names
289 289 imported by that file.
290 290
291 291 Args:
292 292 source: The python source to examine as a string.
293 293 modulename: of specified python source (may have `__init__`)
294 294 localmods: set of locally defined module names (may have `__init__`)
295 295 ignore_nested: If true, import statements that do not start in
296 296 column zero will be ignored.
297 297
298 298 Returns:
299 299 A list of absolute module names imported by the given source.
300 300
301 301 >>> f = 'foo/xxx.py'
302 302 >>> modulename = 'foo.xxx'
303 303 >>> localmods = {'foo.__init__': True,
304 304 ... 'foo.foo1': True, 'foo.foo2': True,
305 305 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
306 306 ... 'baz.__init__': True, 'baz.baz1': True }
307 307 >>> # standard library (= not locally defined ones)
308 308 >>> sorted(imported_modules(
309 309 ... 'from stdlib1 import foo, bar; import stdlib2',
310 310 ... modulename, f, localmods))
311 311 []
312 312 >>> # relative importing
313 313 >>> sorted(imported_modules(
314 314 ... 'import foo1; from bar import bar1',
315 315 ... modulename, f, localmods))
316 316 ['foo.bar.bar1', 'foo.foo1']
317 317 >>> sorted(imported_modules(
318 318 ... 'from bar.bar1 import name1, name2, name3',
319 319 ... modulename, f, localmods))
320 320 ['foo.bar.bar1']
321 321 >>> # absolute importing
322 322 >>> sorted(imported_modules(
323 323 ... 'from baz import baz1, name1',
324 324 ... modulename, f, localmods))
325 325 ['baz.__init__', 'baz.baz1']
326 326 >>> # mixed importing, even though it shouldn't be recommended
327 327 >>> sorted(imported_modules(
328 328 ... 'import stdlib, foo1, baz',
329 329 ... modulename, f, localmods))
330 330 ['baz.__init__', 'foo.foo1']
331 331 >>> # ignore_nested
332 332 >>> sorted(imported_modules(
333 333 ... '''import foo
334 334 ... def wat():
335 335 ... import bar
336 336 ... ''', modulename, f, localmods))
337 337 ['foo.__init__', 'foo.bar.__init__']
338 338 >>> sorted(imported_modules(
339 339 ... '''import foo
340 340 ... def wat():
341 341 ... import bar
342 342 ... ''', modulename, f, localmods, ignore_nested=True))
343 343 ['foo.__init__']
344 344 """
345 345 fromlocal = fromlocalfunc(modulename, localmods)
346 346 for node in ast.walk(ast.parse(source, f)):
347 347 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
348 348 continue
349 349 if isinstance(node, ast.Import):
350 350 for n in node.names:
351 351 found = fromlocal(n.name)
352 352 if not found:
353 353 # this should import standard library
354 354 continue
355 355 yield found[1]
356 356 elif isinstance(node, ast.ImportFrom):
357 357 found = fromlocal(node.module, node.level)
358 358 if not found:
359 359 # this should import standard library
360 360 continue
361 361
362 362 absname, dottedpath, hassubmod = found
363 363 if not hassubmod:
364 364 # "dottedpath" is not a package; must be imported
365 365 yield dottedpath
366 366 # examination of "node.names" should be redundant
367 367 # e.g.: from mercurial.node import nullid, nullrev
368 368 continue
369 369
370 370 modnotfound = False
371 371 prefix = absname + '.'
372 372 for n in node.names:
373 373 found = fromlocal(prefix + n.name)
374 374 if not found:
375 375 # this should be a function or a property of "node.module"
376 376 modnotfound = True
377 377 continue
378 378 yield found[1]
379 379 if modnotfound:
380 380 # "dottedpath" is a package, but imported because of non-module
381 381 # lookup
382 382 yield dottedpath
383 383
384 384 def verify_import_convention(module, source, localmods):
385 385 """Verify imports match our established coding convention.
386 386
387 387 We have 2 conventions: legacy and modern. The modern convention is in
388 388 effect when using absolute imports.
389 389
390 390 The legacy convention only looks for mixed imports. The modern convention
391 391 is much more thorough.
392 392 """
393 393 root = ast.parse(source)
394 394 absolute = usingabsolute(root)
395 395
396 396 if absolute:
397 397 return verify_modern_convention(module, root, localmods)
398 398 else:
399 399 return verify_stdlib_on_own_line(root)
400 400
401 401 def verify_modern_convention(module, root, localmods, root_col_offset=0):
402 402 """Verify a file conforms to the modern import convention rules.
403 403
404 404 The rules of the modern convention are:
405 405
406 406 * Ordering is stdlib followed by local imports. Each group is lexically
407 407 sorted.
408 408 * Importing multiple modules via "import X, Y" is not allowed: use
409 409 separate import statements.
410 410 * Importing multiple modules via "from X import ..." is allowed if using
411 411 parenthesis and one entry per line.
412 412 * Only 1 relative import statement per import level ("from .", "from ..")
413 413 is allowed.
414 414 * Relative imports from higher levels must occur before lower levels. e.g.
415 415 "from .." must be before "from .".
416 416 * Imports from peer packages should use relative import (e.g. do not
417 417 "import mercurial.foo" from a "mercurial.*" module).
418 418 * Symbols can only be imported from specific modules (see
419 419 `allowsymbolimports`). For other modules, first import the module then
420 420 assign the symbol to a module-level variable. In addition, these imports
421 421 must be performed before other local imports. This rule only
422 422 applies to import statements outside of any blocks.
423 423 * Relative imports from the standard library are not allowed, unless that
424 424 library is also a local module.
425 425 * Certain modules must be aliased to alternate names to avoid aliasing
426 426 and readability problems. See `requirealias`.
427 427 """
428 428 if not isinstance(module, str):
429 429 module = module.decode('ascii')
430 430 topmodule = module.split('.')[0]
431 431 fromlocal = fromlocalfunc(module, localmods)
432 432
433 433 # Whether a local/non-stdlib import has been performed.
434 434 seenlocal = None
435 435 # Whether a local/non-stdlib, non-symbol import has been seen.
436 436 seennonsymbollocal = False
437 437 # The last name to be imported (for sorting).
438 438 lastname = None
439 439 laststdlib = None
440 440 # Relative import levels encountered so far.
441 441 seenlevels = set()
442 442
443 443 for node, newscope in walklocal(root):
444 444 def msg(fmt, *args):
445 445 return (fmt % args, node.lineno)
446 446 if newscope:
447 447 # Check for local imports in function
448 448 for r in verify_modern_convention(module, node, localmods,
449 449 node.col_offset + 4):
450 450 yield r
451 451 elif isinstance(node, ast.Import):
452 452 # Disallow "import foo, bar" and require separate imports
453 453 # for each module.
454 454 if len(node.names) > 1:
455 455 yield msg('multiple imported names: %s',
456 456 ', '.join(n.name for n in node.names))
457 457
458 458 name = node.names[0].name
459 459 asname = node.names[0].asname
460 460
461 461 stdlib = name in stdlib_modules
462 462
463 463 # Ignore sorting rules on imports inside blocks.
464 464 if node.col_offset == root_col_offset:
465 465 if lastname and name < lastname and laststdlib == stdlib:
466 466 yield msg('imports not lexically sorted: %s < %s',
467 467 name, lastname)
468 468
469 469 lastname = name
470 470 laststdlib = stdlib
471 471
472 472 # stdlib imports should be before local imports.
473 473 if stdlib and seenlocal and node.col_offset == root_col_offset:
474 474 yield msg('stdlib import "%s" follows local import: %s',
475 475 name, seenlocal)
476 476
477 477 if not stdlib:
478 478 seenlocal = name
479 479
480 480 # Import of sibling modules should use relative imports.
481 481 topname = name.split('.')[0]
482 482 if topname == topmodule:
483 483 yield msg('import should be relative: %s', name)
484 484
485 485 if name in requirealias and asname != requirealias[name]:
486 486 yield msg('%s module must be "as" aliased to %s',
487 487 name, requirealias[name])
488 488
489 489 elif isinstance(node, ast.ImportFrom):
490 490 # Resolve the full imported module name.
491 491 if node.level > 0:
492 492 fullname = '.'.join(module.split('.')[:-node.level])
493 493 if node.module:
494 494 fullname += '.%s' % node.module
495 495 else:
496 496 assert node.module
497 497 fullname = node.module
498 498
499 499 topname = fullname.split('.')[0]
500 500 if topname == topmodule:
501 501 yield msg('import should be relative: %s', fullname)
502 502
503 503 # __future__ is special since it needs to come first and use
504 504 # symbol import.
505 505 if fullname != '__future__':
506 506 if not fullname or (
507 507 fullname in stdlib_modules
508 508 and fullname not in localmods
509 509 and fullname + '.__init__' not in localmods):
510 510 yield msg('relative import of stdlib module')
511 511 else:
512 512 seenlocal = fullname
513 513
514 514 # Direct symbol import is only allowed from certain modules and
515 515 # must occur before non-symbol imports.
516 516 found = fromlocal(node.module, node.level)
517 517 if found and found[2]: # node.module is a package
518 518 prefix = found[0] + '.'
519 519 symbols = (n.name for n in node.names
520 520 if not fromlocal(prefix + n.name))
521 521 else:
522 522 symbols = (n.name for n in node.names)
523 523 symbols = [sym for sym in symbols if sym not in directsymbols]
524 524 if node.module and node.col_offset == root_col_offset:
525 525 if symbols and fullname not in allowsymbolimports:
526 526 yield msg('direct symbol import %s from %s',
527 527 ', '.join(symbols), fullname)
528 528
529 529 if symbols and seennonsymbollocal:
530 530 yield msg('symbol import follows non-symbol import: %s',
531 531 fullname)
532 532 if not symbols and fullname not in stdlib_modules:
533 533 seennonsymbollocal = True
534 534
535 535 if not node.module:
536 536 assert node.level
537 537
538 538 # Only allow 1 group per level.
539 539 if (node.level in seenlevels
540 540 and node.col_offset == root_col_offset):
541 541 yield msg('multiple "from %s import" statements',
542 542 '.' * node.level)
543 543
544 544 # Higher-level groups come before lower-level groups.
545 545 if any(node.level > l for l in seenlevels):
546 546 yield msg('higher-level import should come first: %s',
547 547 fullname)
548 548
549 549 seenlevels.add(node.level)
550 550
551 551 # Entries in "from .X import ( ... )" lists must be lexically
552 552 # sorted.
553 553 lastentryname = None
554 554
555 555 for n in node.names:
556 556 if lastentryname and n.name < lastentryname:
557 557 yield msg('imports from %s not lexically sorted: %s < %s',
558 558 fullname, n.name, lastentryname)
559 559
560 560 lastentryname = n.name
561 561
562 562 if n.name in requirealias and n.asname != requirealias[n.name]:
563 563 yield msg('%s from %s must be "as" aliased to %s',
564 564 n.name, fullname, requirealias[n.name])
565 565
566 566 def verify_stdlib_on_own_line(root):
567 567 """Given some python source, verify that stdlib imports are done
568 568 in separate statements from relative local module imports.
569 569
570 570 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
571 571 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
572 572 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
573 573 []
574 574 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
575 575 []
576 576 """
577 577 for node in ast.walk(root):
578 578 if isinstance(node, ast.Import):
579 579 from_stdlib = {False: [], True: []}
580 580 for n in node.names:
581 581 from_stdlib[n.name in stdlib_modules].append(n.name)
582 582 if from_stdlib[True] and from_stdlib[False]:
583 583 yield ('mixed imports\n stdlib: %s\n relative: %s' %
584 584 (', '.join(sorted(from_stdlib[True])),
585 585 ', '.join(sorted(from_stdlib[False]))), node.lineno)
586 586
587 587 class CircularImport(Exception):
588 588 pass
589 589
590 590 def checkmod(mod, imports):
591 591 shortest = {}
592 592 visit = [[mod]]
593 593 while visit:
594 594 path = visit.pop(0)
595 595 for i in sorted(imports.get(path[-1], [])):
596 596 if len(path) < shortest.get(i, 1000):
597 597 shortest[i] = len(path)
598 598 if i in path:
599 599 if i == path[0]:
600 600 raise CircularImport(path)
601 601 continue
602 602 visit.append(path + [i])
603 603
604 604 def rotatecycle(cycle):
605 605 """arrange a cycle so that the lexicographically first module listed first
606 606
607 607 >>> rotatecycle(['foo', 'bar'])
608 608 ['bar', 'foo', 'bar']
609 609 """
610 610 lowest = min(cycle)
611 611 idx = cycle.index(lowest)
612 612 return cycle[idx:] + cycle[:idx] + [lowest]
613 613
614 614 def find_cycles(imports):
615 615 """Find cycles in an already-loaded import graph.
616 616
617 617 All module names recorded in `imports` should be absolute one.
618 618
619 619 >>> from __future__ import print_function
620 620 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
621 621 ... 'top.bar': ['top.baz', 'sys'],
622 622 ... 'top.baz': ['top.foo'],
623 623 ... 'top.qux': ['top.foo']}
624 624 >>> print('\\n'.join(sorted(find_cycles(imports))))
625 625 top.bar -> top.baz -> top.foo -> top.bar
626 626 top.foo -> top.qux -> top.foo
627 627 """
628 628 cycles = set()
629 629 for mod in sorted(imports.keys()):
630 630 try:
631 631 checkmod(mod, imports)
632 632 except CircularImport as e:
633 633 cycle = e.args[0]
634 634 cycles.add(" -> ".join(rotatecycle(cycle)))
635 635 return cycles
636 636
637 637 def _cycle_sortkey(c):
638 638 return len(c), c
639 639
640 640 def embedded(f, modname, src):
641 641 """Extract embedded python code
642 642
643 643 >>> def _forcestr(thing):
644 644 ... if not isinstance(thing, str):
645 645 ... return thing.decode('ascii')
646 646 ... return thing
647 647 >>> def test(fn, lines):
648 648 ... for s, m, f, l in embedded(fn, b"example", lines):
649 649 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
650 650 ... print(repr(_forcestr(s)))
651 651 >>> lines = [
652 652 ... b'comment',
653 653 ... b' >>> from __future__ import print_function',
654 654 ... b" >>> ' multiline",
655 655 ... b" ... string'",
656 656 ... b' ',
657 657 ... b'comment',
658 658 ... b' $ cat > foo.py <<EOF',
659 659 ... b' > from __future__ import print_function',
660 660 ... b' > EOF',
661 661 ... ]
662 662 >>> test(b"example.t", lines)
663 663 example[2] doctest.py 1
664 664 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
665 665 example[8] foo.py 7
666 666 'from __future__ import print_function\\n'
667 667 """
668 668 errors = []
669 669 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
670 670 if not name:
671 671 # use 'doctest.py', in order to make already existing
672 672 # doctest above pass instantly
673 673 name = 'doctest.py'
674 674 # "starts" is "line number" (1-origin), but embedded() is
675 675 # expected to return "line offset" (0-origin). Therefore, this
676 676 # yields "starts - 1".
677 677 if not isinstance(modname, str):
678 678 modname = modname.decode('utf8')
679 679 yield code, "%s[%d]" % (modname, starts), name, starts - 1
680 680
681 681 def sources(f, modname):
682 682 """Yields possibly multiple sources from a filepath
683 683
684 684 input: filepath, modulename
685 685 yields: script(string), modulename, filepath, linenumber
686 686
687 687 For embedded scripts, the modulename and filepath will be different
688 688 from the function arguments. linenumber is an offset relative to
689 689 the input file.
690 690 """
691 691 py = False
692 692 if not f.endswith('.t'):
693 693 with open(f, 'rb') as src:
694 694 yield src.read(), modname, f, 0
695 695 py = True
696 696 if py or f.endswith('.t'):
697 697 with open(f, 'rb') as src:
698 698 for script, modname, t, line in embedded(f, modname, src):
699 yield script, modname, t, line
699 yield script, modname.encode('utf8'), t, line
700 700
701 701 def main(argv):
702 702 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
703 703 print('Usage: %s {-|file [file] [file] ...}')
704 704 return 1
705 705 if argv[1] == '-':
706 706 argv = argv[:1]
707 707 argv.extend(l.rstrip() for l in sys.stdin.readlines())
708 708 localmodpaths = {}
709 709 used_imports = {}
710 710 any_errors = False
711 711 for source_path in argv[1:]:
712 712 modname = dotted_name_of_path(source_path)
713 713 localmodpaths[modname] = source_path
714 714 localmods = populateextmods(localmodpaths)
715 715 for localmodname, source_path in sorted(localmodpaths.items()):
716 716 if not isinstance(localmodname, bytes):
717 717 # This is only safe because all hg's files are ascii
718 718 localmodname = localmodname.encode('ascii')
719 719 for src, modname, name, line in sources(source_path, localmodname):
720 720 try:
721 721 used_imports[modname] = sorted(
722 722 imported_modules(src, modname, name, localmods,
723 723 ignore_nested=True))
724 724 for error, lineno in verify_import_convention(modname, src,
725 725 localmods):
726 726 any_errors = True
727 727 print('%s:%d: %s' % (source_path, lineno + line, error))
728 728 except SyntaxError as e:
729 729 print('%s:%d: SyntaxError: %s' %
730 730 (source_path, e.lineno + line, e))
731 731 cycles = find_cycles(used_imports)
732 732 if cycles:
733 733 firstmods = set()
734 734 for c in sorted(cycles, key=_cycle_sortkey):
735 735 first = c.split()[0]
736 736 # As a rough cut, ignore any cycle that starts with the
737 737 # same module as some other cycle. Otherwise we see lots
738 738 # of cycles that are effectively duplicates.
739 739 if first in firstmods:
740 740 continue
741 741 print('Import cycle:', c)
742 742 firstmods.add(first)
743 743 any_errors = True
744 744 return any_errors != 0
745 745
746 746 if __name__ == '__main__':
747 747 sys.exit(int(main(sys.argv)))
General Comments 0
You need to be logged in to leave comments. Login now