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