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