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