# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import json import os import signal import subprocess import which from mozprocess import ProcessHandler from mozlint import result here = os.path.abspath(os.path.dirname(__file__)) FLAKE8_REQUIREMENTS_PATH = os.path.join(here, 'flake8', 'flake8_requirements.txt') FLAKE8_NOT_FOUND = """ Could not find flake8! Install flake8 and try again. $ pip install -U --require-hashes -r {} """.strip().format(FLAKE8_REQUIREMENTS_PATH) FLAKE8_INSTALL_ERROR = """ Unable to install correct version of flake8 Try to install it manually with: $ pip install -U --require-hashes -r {} """.strip().format(FLAKE8_REQUIREMENTS_PATH) LINE_OFFSETS = { # continuation line under-indented for hanging indent 'E121': (-1, 2), # continuation line missing indentation or outdented 'E122': (-1, 2), # continuation line over-indented for hanging indent 'E126': (-1, 2), # continuation line over-indented for visual indent 'E127': (-1, 2), # continuation line under-indented for visual indent 'E128': (-1, 2), # continuation line unaligned for hanging indend 'E131': (-1, 2), # expected 1 blank line, found 0 'E301': (-1, 2), # expected 2 blank lines, found 1 'E302': (-2, 3), } """Maps a flake8 error to a lineoffset tuple. The offset is of the form (lineno_offset, num_lines) and is passed to the lineoffset property of `ResultContainer`. """ EXTENSIONS = ['.py', '.lint'] results = [] def process_line(line): # Escape slashes otherwise JSON conversion will not work line = line.replace('\\', '\\\\') try: res = json.loads(line) except ValueError: print('Non JSON output from linter, will not be processed: {}'.format(line)) return if 'code' in res: if res['code'].startswith('W'): res['level'] = 'warning' if res['code'] in LINE_OFFSETS: res['lineoffset'] = LINE_OFFSETS[res['code']] results.append(result.from_linter(LINTER, **res)) def run_process(cmdargs): # flake8 seems to handle SIGINT poorly. Handle it here instead # so we can kill the process without a cryptic traceback. orig = signal.signal(signal.SIGINT, signal.SIG_IGN) proc = ProcessHandler(cmdargs, env=os.environ, processOutputLine=process_line) proc.run() signal.signal(signal.SIGINT, orig) try: proc.wait() except KeyboardInterrupt: proc.kill() def get_flake8_binary(): """ Returns the path of the first flake8 binary available if not found returns None """ binary = os.environ.get('FLAKE8') if binary: return binary try: return which.which('flake8') except which.WhichError: return None def _run_pip(*args): """ Helper function that runs pip with subprocess """ try: subprocess.check_output(['pip'] + list(args), stderr=subprocess.STDOUT) return True except subprocess.CalledProcessError as e: print(e.output) return False def reinstall_flake8(): """ Try to install flake8 at the target version, returns True on success otherwise prints the otuput of the pip command and returns False """ if _run_pip('install', '-U', '--require-hashes', '-r', FLAKE8_REQUIREMENTS_PATH): return True return False def lint(files, **lintargs): if not reinstall_flake8(): print(FLAKE8_INSTALL_ERROR) return 1 binary = get_flake8_binary() cmdargs = [ binary, '--format', '{"path":"%(path)s","lineno":%(row)s,' '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}', ] # Run any paths with a .flake8 file in the directory separately so # it gets picked up. This means only .flake8 files that live in # directories that are explicitly included will be considered. # See bug 1277851 no_config = [] for f in files: if not os.path.isfile(os.path.join(f, '.flake8')): no_config.append(f) continue run_process(cmdargs+[f]) # XXX For some reason passing in --exclude results in flake8 not using # the local .flake8 file. So for now only pass in --exclude if there # is no local config. exclude = lintargs.get('exclude') if exclude: cmdargs += ['--exclude', ','.join(lintargs['exclude'])] if no_config: run_process(cmdargs+no_config) return results LINTER = { 'name': "flake8", 'description': "Python linter", 'include': [ 'python/mozlint', 'taskcluster', 'testing/firefox-ui', 'testing/marionette/client', 'testing/marionette/harness', 'testing/marionette/puppeteer', 'testing/mozbase', 'testing/mochitest', 'testing/talos/', 'tools/lint', ], 'exclude': ["testing/mozbase/mozdevice/mozdevice/Zeroconf.py", 'testing/mochitest/pywebsocket'], 'extensions': EXTENSIONS, 'type': 'external', 'payload': lint, }