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