diff --git a/fabfile/__init__.py b/fabfile/__init__.py index d765ea5f..9aec62bb 100644 --- a/fabfile/__init__.py +++ b/fabfile/__init__.py @@ -17,7 +17,7 @@ For more help on a particular command from fabric.api import env -from fabfile.tasks import code_quality, build_docs, push_docs, clean +from fabfile.tasks import code_quality, build_docs, push_docs, clean, test # Without this, `fab -l` would display the whole docstring as preamble @@ -26,6 +26,7 @@ __doc__ = "" # This list defines which tasks are made available to the user __all__ = [ "code_quality", + "test", "build_docs", "push_docs", "clean", diff --git a/fabfile/lib.py b/fabfile/lib.py index e22af53a..7af40231 100644 --- a/fabfile/lib.py +++ b/fabfile/lib.py @@ -6,8 +6,10 @@ A library of functions and constants for tasks to make use of. import os import sys +import re +from functools import wraps -from fabric.api import run, hide +from fabric.api import run, hide, cd, env from fabric.context_managers import settings, shell_env from fabvenv import virtualenv @@ -18,6 +20,14 @@ VENV_ROOT = os.path.expanduser(os.path.join('~', '.virtualenvs', 'pybitmessage-d PYTHONPATH = os.path.join(PROJECT_ROOT, 'src',) +def coerce_list(value): + """Coerce a value into a list""" + if isinstance(value, str): + return value.split(',') + else: + sys.exit("Bad string value {}".format(value)) + + def coerce_bool(value): """Coerce a value into a boolean""" if isinstance(value, bool): @@ -51,6 +61,29 @@ def flatten(data): return result +def filelist_from_git(rev=None): + """Return a list of files based on git output""" + cmd = 'git diff --name-only' + if rev: + if rev in ['cached', 'staged']: + cmd += ' --{}'.format(rev) + elif rev == 'working': + pass + else: + cmd += ' -r {}'.format(rev) + + with cd(PROJECT_ROOT): + with hide('running', 'stdout'): + results = [] + ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + clean = ansi_escape.sub('', run(cmd)) + clean = re.sub('\n', '', clean) + for line in clean.split('\r'): + if line.endswith(".py"): + results.append(os.path.abspath(line)) + return results + + def pycodestyle(path_to_file): """Run pycodestyle on a file""" with virtualenv(VENV_ROOT): @@ -148,3 +181,20 @@ def get_filtered_pylint_output(path_to_file): not i.startswith('Using config file'), ]) ] + + +def default_hosts(hosts): + """Decorator to apply default hosts to a task""" + + def real_decorator(func): + """Manipulate env""" + env.hosts = env.hosts or hosts + + @wraps(func) + def wrapper(*args, **kwargs): + """Original function called from here""" + return func(*args, **kwargs) + + return wrapper + + return real_decorator diff --git a/fabfile/tasks.py b/fabfile/tasks.py index b7c52c8a..fb05937d 100644 --- a/fabfile/tasks.py +++ b/fabfile/tasks.py @@ -15,7 +15,7 @@ from fabric.contrib.project import rsync_project from fabvenv import virtualenv from fabfile.lib import ( - autopep8, PROJECT_ROOT, VENV_ROOT, coerce_bool, flatten, + autopep8, PROJECT_ROOT, VENV_ROOT, coerce_bool, flatten, filelist_from_git, default_hosts, get_filtered_pycodestyle_output, get_filtered_flake8_output, get_filtered_pylint_output, ) @@ -45,7 +45,7 @@ def get_tool_results(file_list): def print_results(results, top, verbose, details): """Print an item with the appropriate verbosity / detail""" - if verbose: + if verbose and results: print ''.join( [ os.linesep, @@ -160,7 +160,8 @@ def create_dependency_graphs(): @task -def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): +@default_hosts(['localhost']) +def code_quality(verbose=True, details=False, fix=False, filename=None, top=10, rev=None): """ Check code quality. @@ -171,6 +172,8 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): $ fab -H localhost code_quality + :param rev: If not None, act on files changed since this commit. 'cached/staged' and 'working' have special meaning + :type rev: str or None, default None :param top: Display / fix only the top N violating files, a value of 0 will display / fix all files :type top: int, default 10 :param verbose: Display a header and the counts, without this you just get the filenames in order @@ -187,13 +190,14 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): Intended to be temporary until we have improved code quality and have safeguards to maintain it in place. """ + # pylint: disable=too-many-arguments verbose = coerce_bool(verbose) details = coerce_bool(details) fix = coerce_bool(fix) top = int(top) or -1 - file_list = generate_file_list(filename) + file_list = generate_file_list(filename) if not rev else filelist_from_git(rev) results = get_tool_results(file_list) if fix: @@ -207,6 +211,22 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): @task +@default_hosts(['localhost']) +def test(): + """Run tests on the code""" + + with cd(PROJECT_ROOT): + with virtualenv(VENV_ROOT): + + run('pip uninstall -y pybitmessage') + run('python setup.py install') + + run('pybitmessage -t') + run('python setup.py test') + + +@task +@default_hosts(['localhost']) def build_docs(dep_graph=False, apidoc=True): """ Build the documentation locally. @@ -259,6 +279,7 @@ def build_docs(dep_graph=False, apidoc=True): @task +@default_hosts(['localhost']) def push_docs(path=None): """ Upload the generated docs to a public server. @@ -289,6 +310,7 @@ def push_docs(path=None): @task +@default_hosts(['localhost']) def clean(): """Clean up files generated by fabric commands.""" with hide('running', 'stdout'):