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