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