#!/usr/bin/env python

"""
Enforces Python coding standards via pep8, pyflakes and pylint


Installation:
pip install pep8       - style guide
pip install pep257     - for docstrings
pip install pyflakes   - unused imports and variable declarations
pip install plumbum    - used for executing shell commands

This script can be called from the git pre-commit hook with a
--git-precommit option
"""

import os
import pep257
import re
import sys
from plumbum import local, cli, commands

pep8_options = [
    '--max-line-length=105'
]


def lint(to_lint):
    """
    Run all linters against a list of files.

    :param to_lint: a list of files to lint.

    """
    exit_code = 0
    for linter, options in (('pyflakes', []), ('pep8', pep8_options)):
        try:
            output = local[linter](*(options + to_lint))
        except commands.ProcessExecutionError as e:
            output = e.stdout

        if output:
            exit_code = 1
            print "{0} Errors:".format(linter)
            print output

    output = hacked_pep257(to_lint)
    if output:
        exit_code = 1
        print "Docstring Errors:".format(linter.upper())
        print output

    sys.exit(exit_code)


def hacked_pep257(to_lint):
    """
    Check for the presence of docstrings, but ignore some of the options
    """
    def ignore(*args, **kwargs):
        pass

    pep257.check_blank_before_after_class = ignore
    pep257.check_blank_after_last_paragraph = ignore
    pep257.check_blank_after_summary = ignore
    pep257.check_ends_with_period = ignore
    pep257.check_one_liners = ignore
    pep257.check_imperative_mood = ignore

    original_check_return_type = pep257.check_return_type

    def better_check_return_type(def_docstring, context, is_script):
        """
        Ignore private methods
        """
        def_name = context.split()[1]
        if def_name.startswith('_') and not def_name.endswith('__'):
            original_check_return_type(def_docstring, context, is_script)

    pep257.check_return_type = better_check_return_type

    errors = []
    for filename in to_lint:
        with open(filename) as f:
            source = f.read()
            if source:
                errors.extend(pep257.check_source(source, filename))
    return '\n'.join([str(error) for error in sorted(errors)])


class Lint(cli.Application):
    """
    Command line app for VmrunWrapper
    """

    DESCRIPTION = "Lints python with pep8, pep257, and pyflakes"

    git = cli.Flag("--git-precommit", help="Lint only modified git files",
                   default=False)

    def main(self, *directories):
        """
        The actual logic that runs the linters
        """
        if not self.git and len(directories) == 0:
            print ("ERROR: At least one directory must be provided (or the "
                   "--git-precommit flag must be passed.\n")
            self.help()
            return

        if len(directories) > 0:
            find = local['find']
            files = []
            for directory in directories:
                real = os.path.expanduser(directory)
                if not os.path.exists(real):
                    raise ValueError("{0} does not exist".format(directory))
                files.extend(find(real, '-not', '-name', '._*', '-name', '*.py').strip().split('\n'))
        else:
            status = local['git']('status', '--porcelain', '-uno')
            root = local['git']('rev-parse', '--show-toplevel').strip()

            # get all modified or added python files
            modified = re.findall(r"^\s[AM]\s+(\S+\.py)$", status, re.MULTILINE)

            # now just get the path part, which all should be relative to the
            # root
            files = [os.path.join(root, line.split(' ', 1)[-1].strip())
                     for line in modified]

        if len(files) > 0:
            print "Linting {0} python files.\n".format(len(files))
            lint(files)
        else:
            print "No python files found to lint.\n"


if __name__ == "__main__":
    Lint.run()