##// END OF EJS Templates
contrib: fix import-checker to operate on str instead of bytes...
Augie Fackler -
r42580:04eb3c56 default
parent child Browse files
Show More
@@ -1,747 +1,747 b''
1 1 #!/usr/bin/env python
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 import ast
6 6 import collections
7 7 import os
8 8 import sys
9 9
10 10 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 11 # to work when run from a virtualenv. The modules were chosen empirically
12 12 # so that the return value matches the return value without virtualenv.
13 13 if True: # disable lexical sorting checks
14 14 try:
15 15 import BaseHTTPServer as basehttpserver
16 16 except ImportError:
17 17 basehttpserver = None
18 18 import zlib
19 19
20 20 import testparseutil
21 21
22 22 # Whitelist of modules that symbols can be directly imported from.
23 23 allowsymbolimports = (
24 24 '__future__',
25 25 'bzrlib',
26 26 'hgclient',
27 27 'mercurial',
28 28 'mercurial.hgweb.common',
29 29 'mercurial.hgweb.request',
30 30 'mercurial.i18n',
31 31 'mercurial.node',
32 32 # for revlog to re-export constant to extensions
33 33 'mercurial.revlogutils.constants',
34 34 # for cffi modules to re-export pure functions
35 35 'mercurial.pure.base85',
36 36 'mercurial.pure.bdiff',
37 37 'mercurial.pure.mpatch',
38 38 'mercurial.pure.osutil',
39 39 'mercurial.pure.parsers',
40 40 # third-party imports should be directly imported
41 41 'mercurial.thirdparty',
42 42 'mercurial.thirdparty.attr',
43 43 'mercurial.thirdparty.zope',
44 44 'mercurial.thirdparty.zope.interface',
45 45 )
46 46
47 47 # Whitelist of symbols that can be directly imported.
48 48 directsymbols = (
49 49 'demandimport',
50 50 )
51 51
52 52 # Modules that must be aliased because they are commonly confused with
53 53 # common variables and can create aliasing and readability issues.
54 54 requirealias = {
55 55 'ui': 'uimod',
56 56 }
57 57
58 58 def usingabsolute(root):
59 59 """Whether absolute imports are being used."""
60 60 if sys.version_info[0] >= 3:
61 61 return True
62 62
63 63 for node in ast.walk(root):
64 64 if isinstance(node, ast.ImportFrom):
65 65 if node.module == '__future__':
66 66 for n in node.names:
67 67 if n.name == 'absolute_import':
68 68 return True
69 69
70 70 return False
71 71
72 72 def walklocal(root):
73 73 """Recursively yield all descendant nodes but not in a different scope"""
74 74 todo = collections.deque(ast.iter_child_nodes(root))
75 75 yield root, False
76 76 while todo:
77 77 node = todo.popleft()
78 78 newscope = isinstance(node, ast.FunctionDef)
79 79 if not newscope:
80 80 todo.extend(ast.iter_child_nodes(node))
81 81 yield node, newscope
82 82
83 83 def dotted_name_of_path(path):
84 84 """Given a relative path to a source file, return its dotted module name.
85 85
86 86 >>> dotted_name_of_path('mercurial/error.py')
87 87 'mercurial.error'
88 88 >>> dotted_name_of_path('zlibmodule.so')
89 89 'zlib'
90 90 """
91 91 parts = path.replace(os.sep, '/').split('/')
92 92 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
93 93 if parts[-1].endswith('module'):
94 94 parts[-1] = parts[-1][:-6]
95 95 return '.'.join(parts)
96 96
97 97 def fromlocalfunc(modulename, localmods):
98 98 """Get a function to examine which locally defined module the
99 99 target source imports via a specified name.
100 100
101 101 `modulename` is an `dotted_name_of_path()`-ed source file path,
102 102 which may have `.__init__` at the end of it, of the target source.
103 103
104 104 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
105 105 paths of locally defined (= Mercurial specific) modules.
106 106
107 107 This function assumes that module names not existing in
108 108 `localmods` are from the Python standard library.
109 109
110 110 This function returns the function, which takes `name` argument,
111 111 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
112 112 matches against locally defined module. Otherwise, it returns
113 113 False.
114 114
115 115 It is assumed that `name` doesn't have `.__init__`.
116 116
117 117 `absname` is an absolute module name of specified `name`
118 118 (e.g. "hgext.convert"). This can be used to compose prefix for sub
119 119 modules or so.
120 120
121 121 `dottedpath` is a `dotted_name_of_path()`-ed source file path
122 122 (e.g. "hgext.convert.__init__") of `name`. This is used to look
123 123 module up in `localmods` again.
124 124
125 125 `hassubmod` is whether it may have sub modules under it (for
126 126 convenient, even though this is also equivalent to "absname !=
127 127 dottednpath")
128 128
129 129 >>> localmods = {'foo.__init__', 'foo.foo1',
130 130 ... 'foo.bar.__init__', 'foo.bar.bar1',
131 131 ... 'baz.__init__', 'baz.baz1'}
132 132 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
133 133 >>> # relative
134 134 >>> fromlocal('foo1')
135 135 ('foo.foo1', 'foo.foo1', False)
136 136 >>> fromlocal('bar')
137 137 ('foo.bar', 'foo.bar.__init__', True)
138 138 >>> fromlocal('bar.bar1')
139 139 ('foo.bar.bar1', 'foo.bar.bar1', False)
140 140 >>> # absolute
141 141 >>> fromlocal('baz')
142 142 ('baz', 'baz.__init__', True)
143 143 >>> fromlocal('baz.baz1')
144 144 ('baz.baz1', 'baz.baz1', False)
145 145 >>> # unknown = maybe standard library
146 146 >>> fromlocal('os')
147 147 False
148 148 >>> fromlocal(None, 1)
149 149 ('foo', 'foo.__init__', True)
150 150 >>> fromlocal('foo1', 1)
151 151 ('foo.foo1', 'foo.foo1', False)
152 152 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
153 153 >>> fromlocal2(None, 2)
154 154 ('foo', 'foo.__init__', True)
155 155 >>> fromlocal2('bar2', 1)
156 156 False
157 157 >>> fromlocal2('bar', 2)
158 158 ('foo.bar', 'foo.bar.__init__', True)
159 159 """
160 160 if not isinstance(modulename, str):
161 161 modulename = modulename.decode('ascii')
162 162 prefix = '.'.join(modulename.split('.')[:-1])
163 163 if prefix:
164 164 prefix += '.'
165 165 def fromlocal(name, level=0):
166 166 # name is false value when relative imports are used.
167 167 if not name:
168 168 # If relative imports are used, level must not be absolute.
169 169 assert level > 0
170 170 candidates = ['.'.join(modulename.split('.')[:-level])]
171 171 else:
172 172 if not level:
173 173 # Check relative name first.
174 174 candidates = [prefix + name, name]
175 175 else:
176 176 candidates = ['.'.join(modulename.split('.')[:-level]) +
177 177 '.' + name]
178 178
179 179 for n in candidates:
180 180 if n in localmods:
181 181 return (n, n, False)
182 182 dottedpath = n + '.__init__'
183 183 if dottedpath in localmods:
184 184 return (n, dottedpath, True)
185 185 return False
186 186 return fromlocal
187 187
188 188 def populateextmods(localmods):
189 189 """Populate C extension modules based on pure modules"""
190 190 newlocalmods = set(localmods)
191 191 for n in localmods:
192 192 if n.startswith('mercurial.pure.'):
193 193 m = n[len('mercurial.pure.'):]
194 194 newlocalmods.add('mercurial.cext.' + m)
195 195 newlocalmods.add('mercurial.cffi._' + m)
196 196 return newlocalmods
197 197
198 198 def list_stdlib_modules():
199 199 """List the modules present in the stdlib.
200 200
201 201 >>> py3 = sys.version_info[0] >= 3
202 202 >>> mods = set(list_stdlib_modules())
203 203 >>> 'BaseHTTPServer' in mods or py3
204 204 True
205 205
206 206 os.path isn't really a module, so it's missing:
207 207
208 208 >>> 'os.path' in mods
209 209 False
210 210
211 211 sys requires special treatment, because it's baked into the
212 212 interpreter, but it should still appear:
213 213
214 214 >>> 'sys' in mods
215 215 True
216 216
217 217 >>> 'collections' in mods
218 218 True
219 219
220 220 >>> 'cStringIO' in mods or py3
221 221 True
222 222
223 223 >>> 'cffi' in mods
224 224 True
225 225 """
226 226 for m in sys.builtin_module_names:
227 227 yield m
228 228 # These modules only exist on windows, but we should always
229 229 # consider them stdlib.
230 230 for m in ['msvcrt', '_winreg']:
231 231 yield m
232 232 yield '__builtin__'
233 233 yield 'builtins' # python3 only
234 234 yield 'importlib.abc' # python3 only
235 235 yield 'importlib.machinery' # python3 only
236 236 yield 'importlib.util' # python3 only
237 237 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
238 238 yield m
239 239 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
240 240 yield m
241 241 for m in ['cffi']:
242 242 yield m
243 243 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
244 244 # We need to supplement the list of prefixes for the search to work
245 245 # when run from within a virtualenv.
246 246 for mod in (basehttpserver, zlib):
247 247 if mod is None:
248 248 continue
249 249 try:
250 250 # Not all module objects have a __file__ attribute.
251 251 filename = mod.__file__
252 252 except AttributeError:
253 253 continue
254 254 dirname = os.path.dirname(filename)
255 255 for prefix in stdlib_prefixes:
256 256 if dirname.startswith(prefix):
257 257 # Then this directory is redundant.
258 258 break
259 259 else:
260 260 stdlib_prefixes.add(dirname)
261 261 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
262 262 for libpath in sys.path:
263 263 # We want to walk everything in sys.path that starts with something in
264 264 # stdlib_prefixes, but not directories from the hg sources.
265 265 if (os.path.abspath(libpath).startswith(sourceroot)
266 266 or 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 ... b'comment',
653 ... b' >>> from __future__ import print_function',
654 ... b" >>> ' multiline",
655 ... b" ... string'",
656 ... b' ',
657 ... b'comment',
658 ... b' $ cat > foo.py <<EOF',
659 ... b' > from __future__ import print_function',
660 ... b' > EOF',
652 ... 'comment',
653 ... ' >>> from __future__ import print_function',
654 ... " >>> ' multiline",
655 ... " ... string'",
656 ... ' ',
657 ... 'comment',
658 ... ' $ cat > foo.py <<EOF',
659 ... ' > from __future__ import print_function',
660 ... ' > 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 with open(f, 'rb') as src:
697 with open(f, 'r') as src:
698 698 for script, modname, t, line in embedded(f, modname, src):
699 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