Mercurial > public > mercurial-scm > hg
comparison contrib/import-checker.py @ 26965:1fa66d3ad28d
import-checker: reset context to verify convention in function scope
I got the following error by rewriting hgweb/webcommands.py to use
absolute_import. It is false-positive because the import line appears in
"help" function:
hgweb/webcommands.py:1297: higher-level import should come first: mercurial
This patch makes the import checker aware of the function scope and apply
rules recursively.
author | Yuya Nishihara <yuya@tcha.org> |
---|---|
date | Sun, 01 Nov 2015 17:42:03 +0900 |
parents | 5abba2c92da3 |
children | e5be48dd8215 |
comparison
equal
deleted
inserted
replaced
26964:5abba2c92da3 | 26965:1fa66d3ad28d |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 import ast | 3 import ast |
4 import collections | |
4 import os | 5 import os |
5 import sys | 6 import sys |
6 | 7 |
7 # Import a minimal set of stdlib modules needed for list_stdlib_modules() | 8 # Import a minimal set of stdlib modules needed for list_stdlib_modules() |
8 # to work when run from a virtualenv. The modules were chosen empirically | 9 # to work when run from a virtualenv. The modules were chosen empirically |
34 for n in node.names: | 35 for n in node.names: |
35 if n.name == 'absolute_import': | 36 if n.name == 'absolute_import': |
36 return True | 37 return True |
37 | 38 |
38 return False | 39 return False |
40 | |
41 def walklocal(root): | |
42 """Recursively yield all descendant nodes but not in a different scope""" | |
43 todo = collections.deque(ast.iter_child_nodes(root)) | |
44 yield root, False | |
45 while todo: | |
46 node = todo.popleft() | |
47 newscope = isinstance(node, ast.FunctionDef) | |
48 if not newscope: | |
49 todo.extend(ast.iter_child_nodes(node)) | |
50 yield node, newscope | |
39 | 51 |
40 def dotted_name_of_path(path, trimpure=False): | 52 def dotted_name_of_path(path, trimpure=False): |
41 """Given a relative path to a source file, return its dotted module name. | 53 """Given a relative path to a source file, return its dotted module name. |
42 | 54 |
43 >>> dotted_name_of_path('mercurial/error.py') | 55 >>> dotted_name_of_path('mercurial/error.py') |
322 if absolute: | 334 if absolute: |
323 return verify_modern_convention(module, root) | 335 return verify_modern_convention(module, root) |
324 else: | 336 else: |
325 return verify_stdlib_on_own_line(root) | 337 return verify_stdlib_on_own_line(root) |
326 | 338 |
327 def verify_modern_convention(module, root): | 339 def verify_modern_convention(module, root, root_col_offset=0): |
328 """Verify a file conforms to the modern import convention rules. | 340 """Verify a file conforms to the modern import convention rules. |
329 | 341 |
330 The rules of the modern convention are: | 342 The rules of the modern convention are: |
331 | 343 |
332 * Ordering is stdlib followed by local imports. Each group is lexically | 344 * Ordering is stdlib followed by local imports. Each group is lexically |
359 # The last name to be imported (for sorting). | 371 # The last name to be imported (for sorting). |
360 lastname = None | 372 lastname = None |
361 # Relative import levels encountered so far. | 373 # Relative import levels encountered so far. |
362 seenlevels = set() | 374 seenlevels = set() |
363 | 375 |
364 for node in ast.walk(root): | 376 for node, newscope in walklocal(root): |
365 def msg(fmt, *args): | 377 def msg(fmt, *args): |
366 return (fmt % args, node.lineno) | 378 return (fmt % args, node.lineno) |
367 if isinstance(node, ast.Import): | 379 if newscope: |
380 # Check for local imports in function | |
381 for r in verify_modern_convention(module, node, | |
382 node.col_offset + 4): | |
383 yield r | |
384 elif isinstance(node, ast.Import): | |
368 # Disallow "import foo, bar" and require separate imports | 385 # Disallow "import foo, bar" and require separate imports |
369 # for each module. | 386 # for each module. |
370 if len(node.names) > 1: | 387 if len(node.names) > 1: |
371 yield msg('multiple imported names: %s', | 388 yield msg('multiple imported names: %s', |
372 ', '.join(n.name for n in node.names)) | 389 ', '.join(n.name for n in node.names)) |
373 | 390 |
374 name = node.names[0].name | 391 name = node.names[0].name |
375 asname = node.names[0].asname | 392 asname = node.names[0].asname |
376 | 393 |
377 # Ignore sorting rules on imports inside blocks. | 394 # Ignore sorting rules on imports inside blocks. |
378 if node.col_offset == 0: | 395 if node.col_offset == root_col_offset: |
379 if lastname and name < lastname: | 396 if lastname and name < lastname: |
380 yield msg('imports not lexically sorted: %s < %s', | 397 yield msg('imports not lexically sorted: %s < %s', |
381 name, lastname) | 398 name, lastname) |
382 | 399 |
383 lastname = name | 400 lastname = name |
384 | 401 |
385 # stdlib imports should be before local imports. | 402 # stdlib imports should be before local imports. |
386 stdlib = name in stdlib_modules | 403 stdlib = name in stdlib_modules |
387 if stdlib and seenlocal and node.col_offset == 0: | 404 if stdlib and seenlocal and node.col_offset == root_col_offset: |
388 yield msg('stdlib import follows local import: %s', name) | 405 yield msg('stdlib import follows local import: %s', name) |
389 | 406 |
390 if not stdlib: | 407 if not stdlib: |
391 seenlocal = True | 408 seenlocal = True |
392 | 409 |
421 else: | 438 else: |
422 seenlocal = True | 439 seenlocal = True |
423 | 440 |
424 # Direct symbol import is only allowed from certain modules and | 441 # Direct symbol import is only allowed from certain modules and |
425 # must occur before non-symbol imports. | 442 # must occur before non-symbol imports. |
426 if node.module and node.col_offset == 0: | 443 if node.module and node.col_offset == root_col_offset: |
427 if fullname not in allowsymbolimports: | 444 if fullname not in allowsymbolimports: |
428 yield msg('direct symbol import from %s', fullname) | 445 yield msg('direct symbol import from %s', fullname) |
429 | 446 |
430 if seennonsymbolrelative: | 447 if seennonsymbolrelative: |
431 yield msg('symbol import follows non-symbol import: %s', | 448 yield msg('symbol import follows non-symbol import: %s', |
434 if not node.module: | 451 if not node.module: |
435 assert node.level | 452 assert node.level |
436 seennonsymbolrelative = True | 453 seennonsymbolrelative = True |
437 | 454 |
438 # Only allow 1 group per level. | 455 # Only allow 1 group per level. |
439 if node.level in seenlevels and node.col_offset == 0: | 456 if (node.level in seenlevels |
457 and node.col_offset == root_col_offset): | |
440 yield msg('multiple "from %s import" statements', | 458 yield msg('multiple "from %s import" statements', |
441 '.' * node.level) | 459 '.' * node.level) |
442 | 460 |
443 # Higher-level groups come before lower-level groups. | 461 # Higher-level groups come before lower-level groups. |
444 if any(node.level > l for l in seenlevels): | 462 if any(node.level > l for l in seenlevels): |