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