PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/flake8/hooks.py

https://gitlab.com/tomprince/flake8
Python | 295 lines | 282 code | 3 blank | 10 comment | 3 complexity | e6b4d67e198da056691bc1e0b4d8efca MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. from __future__ import with_statement
  3. import os
  4. import pycodestyle as pep8
  5. import sys
  6. import stat
  7. from subprocess import Popen, PIPE
  8. import shutil
  9. import tempfile
  10. try:
  11. from configparser import ConfigParser
  12. except ImportError: # Python 2
  13. from ConfigParser import ConfigParser
  14. from flake8 import compat
  15. from flake8.engine import get_parser, get_style_guide
  16. def git_hook(complexity=-1, strict=False, ignore=None, lazy=False):
  17. """This is the function used by the git hook.
  18. :param int complexity: (optional), any value > 0 enables complexity
  19. checking with mccabe
  20. :param bool strict: (optional), if True, this returns the total number of
  21. errors which will cause the hook to fail
  22. :param str ignore: (optional), a comma-separated list of errors and
  23. warnings to ignore
  24. :param bool lazy: (optional), allows for the instances where you don't add
  25. the files to the index before running a commit, e.g., git commit -a
  26. :returns: total number of errors if strict is True, otherwise 0
  27. """
  28. gitcmd = "git diff-index --cached --name-only --diff-filter=ACMRTUXB HEAD"
  29. if lazy:
  30. # Catch all files, including those not added to the index
  31. gitcmd = gitcmd.replace('--cached ', '')
  32. if hasattr(ignore, 'split'):
  33. ignore = ignore.split(',')
  34. # Returns the exit code, list of files modified, list of error messages
  35. _, files_modified, _ = run(gitcmd)
  36. # We only want to pass ignore and max_complexity if they differ from the
  37. # defaults so that we don't override a local configuration file
  38. options = {}
  39. if ignore:
  40. options['ignore'] = ignore
  41. if complexity > -1:
  42. options['max_complexity'] = complexity
  43. tmpdir = tempfile.mkdtemp()
  44. flake8_style = get_style_guide(paths=['.'], **options)
  45. filepatterns = flake8_style.options.filename
  46. # Copy staged versions to temporary directory
  47. files_to_check = []
  48. try:
  49. for file_ in files_modified:
  50. # get the staged version of the file
  51. gitcmd_getstaged = "git show :%s" % file_
  52. _, out, _ = run(gitcmd_getstaged, raw_output=True, decode=False)
  53. # write the staged version to temp dir with its full path to
  54. # avoid overwriting files with the same name
  55. dirname, filename = os.path.split(os.path.abspath(file_))
  56. prefix = os.path.commonprefix([dirname, tmpdir])
  57. dirname = compat.relpath(dirname, start=prefix)
  58. dirname = os.path.join(tmpdir, dirname)
  59. if not os.path.isdir(dirname):
  60. os.makedirs(dirname)
  61. # check_files() only does this check if passed a dir; so we do it
  62. if ((pep8.filename_match(file_, filepatterns) and
  63. not flake8_style.excluded(file_))):
  64. filename = os.path.join(dirname, filename)
  65. files_to_check.append(filename)
  66. # write staged version of file to temporary directory
  67. with open(filename, "wb") as fh:
  68. fh.write(out)
  69. # Run the checks
  70. report = flake8_style.check_files(files_to_check)
  71. # remove temporary directory
  72. finally:
  73. shutil.rmtree(tmpdir, ignore_errors=True)
  74. if strict:
  75. return report.total_errors
  76. return 0
  77. def hg_hook(ui, repo, **kwargs):
  78. """This is the function executed directly by Mercurial as part of the
  79. hook. This is never called directly by the user, so the parameters are
  80. undocumented. If you would like to learn more about them, please feel free
  81. to read the official Mercurial documentation.
  82. """
  83. complexity = ui.config('flake8', 'complexity', default=-1)
  84. strict = ui.configbool('flake8', 'strict', default=True)
  85. ignore = ui.config('flake8', 'ignore', default=None)
  86. config = ui.config('flake8', 'config', default=None)
  87. paths = _get_files(repo, **kwargs)
  88. # We only want to pass ignore and max_complexity if they differ from the
  89. # defaults so that we don't override a local configuration file
  90. options = {}
  91. if ignore:
  92. options['ignore'] = ignore
  93. if complexity > -1:
  94. options['max_complexity'] = complexity
  95. flake8_style = get_style_guide(config_file=config, paths=['.'],
  96. **options)
  97. report = flake8_style.check_files(paths)
  98. if strict:
  99. return report.total_errors
  100. return 0
  101. def run(command, raw_output=False, decode=True):
  102. p = Popen(command.split(), stdout=PIPE, stderr=PIPE)
  103. (stdout, stderr) = p.communicate()
  104. # On python 3, subprocess.Popen returns bytes objects which expect
  105. # endswith to be given a bytes object or a tuple of bytes but not native
  106. # string objects. This is simply less mysterious than using b'.py' in the
  107. # endswith method. That should work but might still fail horribly.
  108. if decode:
  109. if hasattr(stdout, 'decode'):
  110. stdout = stdout.decode('utf-8')
  111. if hasattr(stderr, 'decode'):
  112. stderr = stderr.decode('utf-8')
  113. if not raw_output:
  114. stdout = [line.strip() for line in stdout.splitlines()]
  115. stderr = [line.strip() for line in stderr.splitlines()]
  116. return (p.returncode, stdout, stderr)
  117. def _get_files(repo, **kwargs):
  118. seen = set()
  119. for rev in range(repo[kwargs['node']], len(repo)):
  120. for file_ in repo[rev].files():
  121. file_ = os.path.join(repo.root, file_)
  122. if file_ in seen or not os.path.exists(file_):
  123. continue
  124. seen.add(file_)
  125. if file_.endswith('.py'):
  126. yield file_
  127. def find_vcs():
  128. try:
  129. _, git_dir, _ = run('git rev-parse --git-dir')
  130. except OSError:
  131. pass
  132. else:
  133. if git_dir and os.path.isdir(git_dir[0]):
  134. if not os.path.isdir(os.path.join(git_dir[0], 'hooks')):
  135. os.mkdir(os.path.join(git_dir[0], 'hooks'))
  136. return os.path.join(git_dir[0], 'hooks', 'pre-commit')
  137. try:
  138. _, hg_dir, _ = run('hg root')
  139. except OSError:
  140. pass
  141. else:
  142. if hg_dir and os.path.isdir(hg_dir[0]):
  143. return os.path.join(hg_dir[0], '.hg', 'hgrc')
  144. return ''
  145. def get_git_config(option, opt_type='', convert_type=True):
  146. # type can be --bool, --int or an empty string
  147. _, git_cfg_value, _ = run('git config --get %s %s' % (opt_type, option),
  148. raw_output=True)
  149. git_cfg_value = git_cfg_value.strip()
  150. if not convert_type:
  151. return git_cfg_value
  152. if opt_type == '--bool':
  153. git_cfg_value = git_cfg_value.lower() == 'true'
  154. elif git_cfg_value and opt_type == '--int':
  155. git_cfg_value = int(git_cfg_value)
  156. return git_cfg_value
  157. _params = {
  158. 'FLAKE8_COMPLEXITY': '--int',
  159. 'FLAKE8_STRICT': '--bool',
  160. 'FLAKE8_IGNORE': '',
  161. 'FLAKE8_LAZY': '--bool',
  162. }
  163. def get_git_param(option, default=''):
  164. global _params
  165. opt_type = _params[option]
  166. param_value = get_git_config(option.lower().replace('_', '.'),
  167. opt_type=opt_type, convert_type=False)
  168. if param_value == '':
  169. param_value = os.environ.get(option, default)
  170. if opt_type == '--bool' and not isinstance(param_value, bool):
  171. param_value = param_value.lower() == 'true'
  172. elif param_value and opt_type == '--int':
  173. param_value = int(param_value)
  174. return param_value
  175. git_hook_file = """#!/usr/bin/env python
  176. import sys
  177. from flake8.hooks import git_hook, get_git_param
  178. # `get_git_param` will retrieve configuration from your local git config and
  179. # then fall back to using the environment variables that the hook has always
  180. # supported.
  181. # For example, to set the complexity, you'll need to do:
  182. # git config flake8.complexity 10
  183. COMPLEXITY = get_git_param('FLAKE8_COMPLEXITY', 10)
  184. STRICT = get_git_param('FLAKE8_STRICT', False)
  185. IGNORE = get_git_param('FLAKE8_IGNORE', None)
  186. LAZY = get_git_param('FLAKE8_LAZY', False)
  187. if __name__ == '__main__':
  188. sys.exit(git_hook(
  189. complexity=COMPLEXITY,
  190. strict=STRICT,
  191. ignore=IGNORE,
  192. lazy=LAZY,
  193. ))
  194. """
  195. def _install_hg_hook(path):
  196. getenv = os.environ.get
  197. if not os.path.isfile(path):
  198. # Make the file so we can avoid IOError's
  199. open(path, 'w').close()
  200. c = ConfigParser()
  201. c.readfp(open(path, 'r'))
  202. if not c.has_section('hooks'):
  203. c.add_section('hooks')
  204. if not c.has_option('hooks', 'commit'):
  205. c.set('hooks', 'commit', 'python:flake8.hooks.hg_hook')
  206. if not c.has_option('hooks', 'qrefresh'):
  207. c.set('hooks', 'qrefresh', 'python:flake8.hooks.hg_hook')
  208. if not c.has_section('flake8'):
  209. c.add_section('flake8')
  210. if not c.has_option('flake8', 'complexity'):
  211. c.set('flake8', 'complexity', str(getenv('FLAKE8_COMPLEXITY', 10)))
  212. if not c.has_option('flake8', 'strict'):
  213. c.set('flake8', 'strict', getenv('FLAKE8_STRICT', False))
  214. if not c.has_option('flake8', 'ignore'):
  215. c.set('flake8', 'ignore', getenv('FLAKE8_IGNORE', ''))
  216. if not c.has_option('flake8', 'lazy'):
  217. c.set('flake8', 'lazy', getenv('FLAKE8_LAZY', False))
  218. with open(path, 'w') as fd:
  219. c.write(fd)
  220. def install_hook():
  221. vcs = find_vcs()
  222. if not vcs:
  223. p = get_parser()[0]
  224. sys.stderr.write('Error: could not find either a git or mercurial '
  225. 'directory. Please re-run this in a proper '
  226. 'repository.\n')
  227. p.print_help()
  228. sys.exit(1)
  229. status = 0
  230. if 'git' in vcs:
  231. if os.path.exists(vcs):
  232. sys.exit('Error: hook already exists (%s)' % vcs)
  233. with open(vcs, 'w') as fd:
  234. fd.write(git_hook_file)
  235. # rwxr--r--
  236. os.chmod(vcs, stat.S_IRWXU | stat.S_IRGRP | stat.S_IROTH)
  237. elif 'hg' in vcs:
  238. _install_hg_hook(vcs)
  239. else:
  240. status = 1
  241. sys.exit(status)