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