/ test / lint / lint-python.py
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()