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