qutebrowser/scripts/run_checks.py
2015-03-26 07:08:54 +01:00

393 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Run different codecheckers over a codebase.
Runs flake8, pylint, pep257, a CRLF/whitespace/conflict-checker and
pyroma/check-manifest by default.
Module attributes:
option: A dictionary with options.
"""
import sys
import subprocess
import os
import io
import os.path
import unittest
import tokenize
import configparser
import argparse
import collections
import functools
import contextlib
import traceback
import pep257
import coverage
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils
config = configparser.ConfigParser()
@contextlib.contextmanager
def _adjusted_pythonpath(name):
"""Adjust PYTHONPATH for pylint."""
if name == 'pylint':
scriptdir = os.path.abspath(os.path.dirname(__file__))
if 'PYTHONPATH' in os.environ:
old_pythonpath = os.environ['PYTHONPATH']
os.environ['PYTHONPATH'] += os.pathsep + scriptdir
else:
old_pythonpath = None
os.environ['PYTHONPATH'] = scriptdir
yield
if name == 'pylint':
if old_pythonpath is not None:
os.environ['PYTHONPATH'] = old_pythonpath
else:
del os.environ['PYTHONPATH']
def run(name, target=None, print_version=False):
"""Run a checker via distutils with optional args.
Arguments:
name: Name of the checker/binary
target: The package to check
print_version: Whether to print the checker version.
"""
args = _get_args(name)
if target is not None:
args.append(target)
with _adjusted_pythonpath(name):
if os.name == 'nt':
exename = name + '.exe'
else:
exename = name
# for virtualenvs
executable = os.path.join(os.path.dirname(sys.executable), exename)
if not os.path.exists(executable):
# in $PATH
executable = name
if print_version:
subprocess.call([executable, '--version'])
try:
status = subprocess.call([executable] + args)
except OSError:
traceback.print_exc()
status = None
print()
return status
def check_pep257(target, print_version=False):
"""Run pep257 checker with args passed.
We use this rather than run() because on some systems (e.g. Windows) no
pep257 binary is available.
"""
if print_version:
print(pep257.__version__)
args = _get_args('pep257')
sys.argv = ['pep257', target]
if args is not None:
sys.argv += args
try:
# pylint: disable=assignment-from-no-return,no-member
if hasattr(pep257, 'run_pep257'):
# newer pep257 versions
status = pep257.run_pep257()
else:
# older pep257 versions
status = pep257.main(*pep257.parse_options())
print()
return status
except Exception:
traceback.print_exc()
return None
def check_init(target):
"""Check if every subdir of target has an __init__.py file."""
ok = True
for dirpath, _dirnames, filenames in os.walk(target):
if any(f.endswith('.py') for f in filenames):
if '__init__.py' not in filenames:
utils.print_col("Missing __init__.py in {}!".format(dirpath),
'red')
ok = False
print()
return ok
def check_unittest(run_coverage, verbose):
"""Run the unittest checker.
Args:
run_coverage: Whether to also run coverage.py.
verbose: For verbose output.
"""
if run_coverage:
cov = coverage.coverage(branch=True, source=['qutebrowser'])
cov.erase()
cov.start()
suite = unittest.TestLoader().discover('.')
verbosity = 2 if verbose else 1
result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
if run_coverage:
cov.stop()
perc = cov.report(file=io.StringIO())
print("COVERAGE: {}%".format(round(perc)))
cov.html_report()
print()
return result.wasSuccessful()
def check_git():
"""Check for uncommited git files.."""
if not os.path.isdir(".git"):
print("No .git dir, ignoring")
print()
return False
untracked = []
changed = []
gitst = subprocess.check_output(['git', 'status', '--porcelain'])
gitst = gitst.decode('UTF-8').strip()
for line in gitst.splitlines():
s, name = line.split(maxsplit=1)
if s == '??' and name != '.venv/':
untracked.append(name)
elif s == 'M':
changed.append(name)
status = True
if untracked:
status = False
utils.print_col("Untracked files:", 'red')
print('\n'.join(untracked))
if changed:
status = False
utils.print_col("Uncommited changes:", 'red')
print('\n'.join(changed))
print()
return status
def check_vcs_conflict(target):
"""Check VCS conflict markers."""
try:
ok = True
for (dirpath, _dirnames, filenames) in os.walk(target):
for name in (e for e in filenames if e.endswith('.py')):
fn = os.path.join(dirpath, name)
with tokenize.open(fn) as f:
for line in f:
if any(line.startswith(c * 7) for c in '<>=|'):
print("Found conflict marker in {}".format(fn))
ok = False
print()
return ok
except Exception:
traceback.print_exc()
return None
def _get_optional_args(checker):
"""Get a list of arguments based on a comma-separated args config."""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/487/
try:
return config.get(checker, 'args').split(',')
except configparser.NoOptionError:
return []
def _get_flag(arg, checker, option):
"""Get a list of arguments based on a config option."""
try:
return ['--{}={}'.format(arg, config.get(checker, option))]
except configparser.NoOptionError:
return []
def _get_args(checker):
"""Construct the arguments for a given checker.
Return:
A list of commandline arguments.
"""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/487/
args = []
if checker == 'pylint':
args += _get_flag('disable', 'pylint', 'disable')
args += _get_flag('ignore', 'pylint', 'exclude')
args += _get_optional_args('pylint')
plugins = []
for plugin in config.get('pylint', 'plugins').split(','):
plugins.append('pylint_checkers.{}'.format(plugin))
args.append('--load-plugins={}'.format(','.join(plugins)))
elif checker == 'flake8':
args += _get_flag('ignore', 'flake8', 'disable')
args += _get_flag('exclude', 'flake8', 'exclude')
args += _get_optional_args('flake8')
elif checker == 'pep257':
args += _get_flag('ignore', 'pep257', 'disable')
try:
excluded = config.get('pep257', 'exclude').split(',')
except configparser.NoOptionError:
excluded = []
if os.name == 'nt':
# FIXME find a better solution
# pep257 uses cp1252 by default on Windows, which can't handle the
# unicode chars in some files.
# https://github.com/The-Compiler/qutebrowser/issues/105
excluded += ['configdata', 'misc']
args.append(r'--match=(?!{})\.py'.format('|'.join(excluded)))
args += _get_optional_args('pep257')
elif checker == 'pyroma':
args = ['.']
elif checker == 'check-manifest':
args = []
return args
def _get_checkers(args):
"""Get a dict of checkers we need to execute."""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/487/
# "Static" checkers
checkers = collections.OrderedDict([
('global', collections.OrderedDict([
('unittest', functools.partial(check_unittest, args.coverage,
args.verbose)),
('git', check_git),
])),
('setup', collections.OrderedDict([
('pyroma', functools.partial(run, 'pyroma',
print_version=args.version)),
('check-manifest', functools.partial(run, 'check-manifest',
print_version=args.version)),
])),
])
# "Dynamic" checkers which exist once for each target.
for target in config.get('DEFAULT', 'targets').split(','):
checkers[target] = collections.OrderedDict([
('init', functools.partial(check_init, target)),
('pep257', functools.partial(check_pep257, target, args.version)),
('flake8', functools.partial(run, 'flake8', target, args.version)),
('vcs', functools.partial(check_vcs_conflict, target)),
('pylint', functools.partial(run, 'pylint', target, args.version)),
])
return checkers
def _checker_enabled(args, group, name):
"""Check if a named checker is enabled."""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/487/
if args.checkers == 'all':
if not args.setup and group == 'setup':
return False
else:
return True
else:
return name in args.checkers.split(',')
def _parse_args():
"""Parse commandline args via argparse."""
parser = argparse.ArgumentParser(description='Run various checkers.')
parser.add_argument('-c', '--coverage', help="Also run coverage.py and "
"generate a HTML report.", action='store_true')
parser.add_argument('-s', '--setup', help="Run additional setup checks",
action='store_true')
parser.add_argument('-q', '--quiet',
help="Don't print unnecessary headers.",
action='store_true')
parser.add_argument('-V', '--version',
help="Print checker versions.", action='store_true')
parser.add_argument('-v', '--verbose', help="Run some checkers verbose.",
action='store_true')
parser.add_argument('checkers', help="Checkers to run (or 'all')",
default='all', nargs='?')
return parser.parse_args()
def main():
"""Main entry point."""
# pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/487/
utils.change_cwd()
read_files = config.read('.run_checks')
if not read_files:
raise OSError("Could not read config!")
exit_status = collections.OrderedDict()
exit_status_bool = {}
args = _parse_args()
checkers = _get_checkers(args)
groups = ['global']
groups += config.get('DEFAULT', 'targets').split(',')
groups.append('setup')
for group in groups:
print()
utils.print_title(group)
for name, func in checkers[group].items():
if _checker_enabled(args, group, name):
utils.print_subtitle(name)
status = func()
key = '{}_{}'.format(group, name)
exit_status[key] = status
if name == 'flake8':
# pyflakes uses True for errors and False for ok.
exit_status_bool[key] = not status
elif isinstance(status, bool):
exit_status_bool[key] = status
else:
# sys.exit(0) means no problems -> True, anything != 0
# means problems.
exit_status_bool[key] = (status == 0)
elif not args.quiet:
utils.print_subtitle(name)
utils.print_col("Checker disabled.", 'blue')
print()
utils.print_col("Exit status values:", 'yellow')
for (k, v) in exit_status.items():
ok = exit_status_bool[k]
color = 'green' if ok else 'red'
utils.print_col(
' {} - {} ({})'.format(k, 'ok' if ok else 'FAIL', v), color)
if all(exit_status_bool.values()):
return 0
else:
return 1
if __name__ == '__main__':
sys.exit(main())