flake8.lint 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
  2. # This Source Code Form is subject to the terms of the Mozilla Public
  3. # License, v. 2.0. If a copy of the MPL was not distributed with this
  4. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  5. import json
  6. import os
  7. import signal
  8. import subprocess
  9. import which
  10. from mozprocess import ProcessHandler
  11. from mozlint import result
  12. here = os.path.abspath(os.path.dirname(__file__))
  13. FLAKE8_REQUIREMENTS_PATH = os.path.join(here, 'flake8', 'flake8_requirements.txt')
  14. FLAKE8_NOT_FOUND = """
  15. Could not find flake8! Install flake8 and try again.
  16. $ pip install -U --require-hashes -r {}
  17. """.strip().format(FLAKE8_REQUIREMENTS_PATH)
  18. FLAKE8_INSTALL_ERROR = """
  19. Unable to install correct version of flake8
  20. Try to install it manually with:
  21. $ pip install -U --require-hashes -r {}
  22. """.strip().format(FLAKE8_REQUIREMENTS_PATH)
  23. LINE_OFFSETS = {
  24. # continuation line under-indented for hanging indent
  25. 'E121': (-1, 2),
  26. # continuation line missing indentation or outdented
  27. 'E122': (-1, 2),
  28. # continuation line over-indented for hanging indent
  29. 'E126': (-1, 2),
  30. # continuation line over-indented for visual indent
  31. 'E127': (-1, 2),
  32. # continuation line under-indented for visual indent
  33. 'E128': (-1, 2),
  34. # continuation line unaligned for hanging indend
  35. 'E131': (-1, 2),
  36. # expected 1 blank line, found 0
  37. 'E301': (-1, 2),
  38. # expected 2 blank lines, found 1
  39. 'E302': (-2, 3),
  40. }
  41. """Maps a flake8 error to a lineoffset tuple.
  42. The offset is of the form (lineno_offset, num_lines) and is passed
  43. to the lineoffset property of `ResultContainer`.
  44. """
  45. EXTENSIONS = ['.py', '.lint']
  46. results = []
  47. def process_line(line):
  48. # Escape slashes otherwise JSON conversion will not work
  49. line = line.replace('\\', '\\\\')
  50. try:
  51. res = json.loads(line)
  52. except ValueError:
  53. print('Non JSON output from linter, will not be processed: {}'.format(line))
  54. return
  55. if 'code' in res:
  56. if res['code'].startswith('W'):
  57. res['level'] = 'warning'
  58. if res['code'] in LINE_OFFSETS:
  59. res['lineoffset'] = LINE_OFFSETS[res['code']]
  60. results.append(result.from_linter(LINTER, **res))
  61. def run_process(cmdargs):
  62. # flake8 seems to handle SIGINT poorly. Handle it here instead
  63. # so we can kill the process without a cryptic traceback.
  64. orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
  65. proc = ProcessHandler(cmdargs, env=os.environ,
  66. processOutputLine=process_line)
  67. proc.run()
  68. signal.signal(signal.SIGINT, orig)
  69. try:
  70. proc.wait()
  71. except KeyboardInterrupt:
  72. proc.kill()
  73. def get_flake8_binary():
  74. """
  75. Returns the path of the first flake8 binary available
  76. if not found returns None
  77. """
  78. binary = os.environ.get('FLAKE8')
  79. if binary:
  80. return binary
  81. try:
  82. return which.which('flake8')
  83. except which.WhichError:
  84. return None
  85. def _run_pip(*args):
  86. """
  87. Helper function that runs pip with subprocess
  88. """
  89. try:
  90. subprocess.check_output(['pip'] + list(args),
  91. stderr=subprocess.STDOUT)
  92. return True
  93. except subprocess.CalledProcessError as e:
  94. print(e.output)
  95. return False
  96. def reinstall_flake8():
  97. """
  98. Try to install flake8 at the target version, returns True on success
  99. otherwise prints the otuput of the pip command and returns False
  100. """
  101. if _run_pip('install', '-U',
  102. '--require-hashes', '-r',
  103. FLAKE8_REQUIREMENTS_PATH):
  104. return True
  105. return False
  106. def lint(files, **lintargs):
  107. if not reinstall_flake8():
  108. print(FLAKE8_INSTALL_ERROR)
  109. return 1
  110. binary = get_flake8_binary()
  111. cmdargs = [
  112. binary,
  113. '--format', '{"path":"%(path)s","lineno":%(row)s,'
  114. '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
  115. ]
  116. # Run any paths with a .flake8 file in the directory separately so
  117. # it gets picked up. This means only .flake8 files that live in
  118. # directories that are explicitly included will be considered.
  119. # See bug 1277851
  120. no_config = []
  121. for f in files:
  122. if not os.path.isfile(os.path.join(f, '.flake8')):
  123. no_config.append(f)
  124. continue
  125. run_process(cmdargs+[f])
  126. # XXX For some reason passing in --exclude results in flake8 not using
  127. # the local .flake8 file. So for now only pass in --exclude if there
  128. # is no local config.
  129. exclude = lintargs.get('exclude')
  130. if exclude:
  131. cmdargs += ['--exclude', ','.join(lintargs['exclude'])]
  132. if no_config:
  133. run_process(cmdargs+no_config)
  134. return results
  135. LINTER = {
  136. 'name': "flake8",
  137. 'description': "Python linter",
  138. 'include': [
  139. 'python/mozlint',
  140. 'taskcluster',
  141. 'testing/firefox-ui',
  142. 'testing/marionette/client',
  143. 'testing/marionette/harness',
  144. 'testing/marionette/puppeteer',
  145. 'testing/mozbase',
  146. 'testing/mochitest',
  147. 'testing/talos/',
  148. 'tools/lint',
  149. ],
  150. 'exclude': ["testing/mozbase/mozdevice/mozdevice/Zeroconf.py",
  151. 'testing/mochitest/pywebsocket'],
  152. 'extensions': EXTENSIONS,
  153. 'type': 'external',
  154. 'payload': lint,
  155. }