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