lint-python.py
1 #!/usr/bin/env python3 2 # 3 # Copyright (c) 2022 The Bitcoin Core developers 4 # Distributed under the MIT software license, see the accompanying 5 # file COPYING or http://www.opensource.org/licenses/mit-license.php. 6 7 """ 8 Check for specified flake8 and mypy warnings in python files. 9 """ 10 11 import os 12 from pathlib import Path 13 import subprocess 14 import sys 15 16 from importlib.metadata import metadata, PackageNotFoundError 17 18 # Customize mypy cache dir via environment variable 19 cache_dir = Path(__file__).parent.parent / ".mypy_cache" 20 os.environ["MYPY_CACHE_DIR"] = str(cache_dir) 21 22 DEPS = ['flake8', 'lief', 'mypy', 'pyzmq'] 23 24 # All .py files, except those in src/ (to exclude subtrees there) 25 FLAKE_FILES_ARGS = ['git', 'ls-files', '*.py', ':!:src/*.py'] 26 27 # Only .py files in test/functional and contrib/devtools have type annotations 28 # enforced. 29 MYPY_FILES_ARGS = ['git', 'ls-files', 'test/functional/*.py', 'contrib/devtools/*.py'] 30 31 ENABLED = ( 32 'E101,' # indentation contains mixed spaces and tabs 33 'E112,' # expected an indented block 34 'E113,' # unexpected indentation 35 'E115,' # expected an indented block (comment) 36 'E116,' # unexpected indentation (comment) 37 'E125,' # continuation line with same indent as next logical line 38 'E129,' # visually indented line with same indent as next logical line 39 'E131,' # continuation line unaligned for hanging indent 40 'E133,' # closing bracket is missing indentation 41 'E223,' # tab before operator 42 'E224,' # tab after operator 43 'E242,' # tab after ',' 44 'E266,' # too many leading '#' for block comment 45 'E271,' # multiple spaces after keyword 46 'E272,' # multiple spaces before keyword 47 'E273,' # tab after keyword 48 'E274,' # tab before keyword 49 'E275,' # missing whitespace after keyword 50 'E304,' # blank lines found after function decorator 51 'E306,' # expected 1 blank line before a nested definition 52 'E401,' # multiple imports on one line 53 'E402,' # module level import not at top of file 54 'E502,' # the backslash is redundant between brackets 55 'E701,' # multiple statements on one line (colon) 56 'E702,' # multiple statements on one line (semicolon) 57 'E703,' # statement ends with a semicolon 58 'E711,' # comparison to None should be 'if cond is None:' 59 'E714,' # test for object identity should be "is not" 60 'E721,' # do not compare types, use "isinstance()" 61 'E722,' # do not use bare 'except' 62 'E742,' # do not define classes named "l", "O", or "I" 63 'E743,' # do not define functions named "l", "O", or "I" 64 'E901,' # SyntaxError: invalid syntax 65 'E902,' # TokenError: EOF in multi-line string 66 'F401,' # module imported but unused 67 'F402,' # import module from line N shadowed by loop variable 68 'F403,' # 'from foo_module import *' used; unable to detect undefined names 69 'F404,' # future import(s) name after other statements 70 'F405,' # foo_function may be undefined, or defined from star imports: bar_module 71 'F406,' # "from module import *" only allowed at module level 72 'F407,' # an undefined __future__ feature name was imported 73 'F601,' # dictionary key name repeated with different values 74 'F602,' # dictionary key variable name repeated with different values 75 'F621,' # too many expressions in an assignment with star-unpacking 76 'F622,' # two or more starred expressions in an assignment (a, *b, *c = d) 77 'F631,' # assertion test is a tuple, which are always True 78 'F632,' # use ==/!= to compare str, bytes, and int literals 79 'F701,' # a break statement outside of a while or for loop 80 'F702,' # a continue statement outside of a while or for loop 81 'F703,' # a continue statement in a finally block in a loop 82 'F704,' # a yield or yield from statement outside of a function 83 'F705,' # a return statement with arguments inside a generator 84 'F706,' # a return statement outside of a function/method 85 'F707,' # an except: block as not the last exception handler 86 'F811,' # redefinition of unused name from line N 87 'F812,' # list comprehension redefines 'foo' from line N 88 'F821,' # undefined name 'Foo' 89 'F822,' # undefined name name in __all__ 90 'F823,' # local variable name … referenced before assignment 91 'F831,' # duplicate argument name in function definition 92 'F841,' # local variable 'foo' is assigned to but never used 93 'W191,' # indentation contains tabs 94 'W291,' # trailing whitespace 95 'W292,' # no newline at end of file 96 'W293,' # blank line contains whitespace 97 'W601,' # .has_key() is deprecated, use "in" 98 'W602,' # deprecated form of raising exception 99 'W603,' # "<>" is deprecated, use "!=" 100 'W604,' # backticks are deprecated, use "repr()" 101 'W605,' # invalid escape sequence "x" 102 'W606,' # 'async' and 'await' are reserved keywords starting with Python 3.7 103 ) 104 105 106 def check_dependencies(): 107 for dep in DEPS: 108 try: 109 metadata(dep) 110 except PackageNotFoundError: 111 print(f"Skipping Python linting since {dep} is not installed.") 112 exit(0) 113 114 115 def main(): 116 check_dependencies() 117 118 if len(sys.argv) > 1: 119 flake8_files = sys.argv[1:] 120 else: 121 flake8_files = subprocess.check_output(FLAKE_FILES_ARGS).decode("utf-8").splitlines() 122 123 flake8_args = ['flake8', '--ignore=B,C,E,F,I,N,W', f'--select={ENABLED}'] + flake8_files 124 flake8_env = os.environ.copy() 125 flake8_env["PYTHONWARNINGS"] = "ignore" 126 127 try: 128 subprocess.check_call(flake8_args, env=flake8_env) 129 except subprocess.CalledProcessError: 130 exit(1) 131 132 mypy_files = subprocess.check_output(MYPY_FILES_ARGS).decode("utf-8").splitlines() 133 mypy_args = ['mypy', '--show-error-codes'] + mypy_files 134 135 try: 136 subprocess.check_call(mypy_args) 137 except subprocess.CalledProcessError: 138 exit(1) 139 140 141 if __name__ == "__main__": 142 main()