It's all coming together.

This commit is contained in:
Elf M. Sternberg 2016-09-28 14:26:06 -07:00
parent 33c3d7a189
commit 9029eef623
7 changed files with 247 additions and 175 deletions

65
git_lint/__main__.py Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python
from .options import OPTIONS
from .option_handler import cleanup_options
from .reporters import print_report, print_help
from .git_lint import load_config, run_linters, git_base
from getopt import GetoptError
import gettext
_ = gettext.gettext
def main(*args):
if git_base is None:
sys.exit(_('A git repository was not found.'))
(options, filenames, excluded_commands) = cleanup_options(OPTIONS, args)
if len(excluded_commands) > 0:
print(_('These command line options were ignored due to option precedence.'))
for exc in excluded_commands:
print("\t{}".format(exc))
try:
config = load_config(options, git_base)
if 'help' in options:
print_help(OPTIONS, NAME)
return 0
if 'version' in options:
from .reporters get print_version
print_version(NAME, VERSION)
return 0
if 'linters' in options:
from .gitSlint import get_linter_status
working_linter_names, broken_linter_names = get_linter_status(config)
print_linters(working_linter_names, broken_linter_names))
return 0
(results,
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames) = run_linters(options, config, filenames)
print_report(results,
unlintable_filenames,
cant_lint_filenames,
broken_linter_names,
unfindable_filenames,
options)
if not len(results):
return 0
return max([i[2] for i in results if len(i)])
except GetoptError as err:
print_help(OPTIONS)
return 1
if __name__ == '__main__':
import sys
sys.exit(main(*sys.argv))

View File

@ -21,13 +21,6 @@ VERSION = '0.0.4'
NAME = 'git-lint'
# ___ _ _ _
# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___
# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_)
# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___|
#
# ___ __ _ ___ _
# / __|___ _ _ / _(_)__ _ | _ \___ __ _ __| |___ _ _
# | (__/ _ \ ' \| _| / _` | | / -_) _` / _` / -_) '_|
@ -69,7 +62,7 @@ def find_config_file(options, base):
# (commandLineDictionary, repositoryLocation) -> (configurationDictionary | exit)
def get_config(options, base):
def load_config(options, base):
"""Loads the git-lint configuration file.
Returns the configuration file as a dictionary of dictionaries.
@ -161,21 +154,21 @@ def base_file_cleaner(files):
return [file.replace(git_base + '/', '', 1) for file in files]
def make_match_filter_matcher(extensions):
trimmed = [s.strip() for s in reduce(operator.add,
[ex.split(',') for ex in extensions], [])]
cleaned = [re.sub(r'^\.', '', s) for s in trimmed]
return re.compile(r'\.' + '|'.join(cleaned) + r'$')
class MatchFilter:
def __init__(self, config):
self.matcher = self.make_match_filter_matcher([v.linter.get('match', '') for v in config])
def make_match_filter(config):
matcher = make_match_filter_matcher([v.linter.get('match', '') for v in config])
def match_filter(path):
return matcher.search(path)
return match_filter
def __call__(self, path):
return self.matcher.search(path)
@staticmethod
def make_match_filter_matcher(extensions):
trimmed = [s.strip() for s in reduce(operator.add,
[ex.split(',') for ex in extensions], [])]
cleaned = [re.sub(r'^\.', '', s) for s in trimmed]
return re.compile(r'\.' + '|'.join(cleaned) + r'$')
# ICK. Mutation, references, and hidden assignment.
def group_by(iterable, field_id):
@ -247,7 +240,7 @@ def print_linters(config):
# \___\___|\__| |_|_/__/\__| \___/_| |_| |_|_\___/__/
#
def get_filelist(cmdline, extras):
def get_filelist(options, extras):
""" Returns the list of files against which we'll run the linters. """
def base_file_filter(files):
@ -331,13 +324,13 @@ def get_filelist(cmdline, extras):
return ([os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)], not_found)
working_directory_trans = cwd_file_filter
if 'base' in cmdline or 'every' in cmdline:
if 'base' in options or 'every' in options:
working_directory_trans = base_file_filter
file_list_generator = working_list
if 'all' in cmdline:
if 'all' in options:
file_list_generator = all_list
if 'staging' in cmdline:
if 'staging' in options:
file_list_generator = staging_list
return (working_directory_trans(remove_submodules(file_list_generator())), [])
@ -349,16 +342,15 @@ def get_filelist(cmdline, extras):
# |___/\__\__,_\__, |_|_||_\__, | \_/\_/|_| \__,_| .__/ .__/\___|_|
# |___/ |___/ |_| |_|
def pick_stash_runner(cmdline):
"""Choose a runner.
This is the operation that will run the linters. It exists to
provide a way to stash the repository, then restore it when
complete. If possible, it attempts to restore the access and
modification times of the file in order to comfort IDEs that are
constantly monitoring file times.
"""
class Runner:
def __init__(self, options):
self.runner = ('staging' in options and self.staging_wrapper) or self.workspace_wrapper
def __call__(run_linters, filenames):
return self.runner(run_linters, filenames)
@staticmethod
def staging_wrapper(run_linters, filenames):
def time_gather(f):
stats = os.stat(f)
@ -375,11 +367,10 @@ def pick_stash_runner(cmdline):
os.utime(filename, timepair)
return results
@staticmethod
def workspace_wrapper(run_linters, filenames):
return run_linters()
return ('staging' in cmdline and staging_wrapper) or workspace_wrapper
# ___ _ _ _
# | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______
@ -387,131 +378,87 @@ def pick_stash_runner(cmdline):
# |_|_\\_,_|_||_| |_|_|_||_\__| | .__/\__,_/__/__/
# |_|
def run_external_linter(filename, linter, linter_name):
"""Run one linter against one file.
If the result matches the error condition specified in the
configuration file, return the error code and messages, either
return nothing.
"""
class Linter:
def __init__(linters, filenames):
self.linters = linters
self.filenames = filenames
@staticmethod
def encode_shell_messages(prefix, messages):
return ['{}{}'.format(prefix, line)
for line in messages.splitlines()]
cmd = linter['command'] + ' "' + filename + '"'
(out, err, returncode) = get_shell_response(cmd)
failed = ((out and (linter.get('condition', 'error') == 'output')) or err or (not (returncode == 0)))
trimmed_filename = filename.replace(git_base + '/', '', 1)
if not failed:
return (trimmed_filename, linter_name, 0, [])
@staticmethod
def run_external_linter(filename, linter, linter_name):
"""Run one linter against one file.
prefix = (((linter.get('print', 'false').strip().lower() != 'true') and ' ')
or ' {}: '.format(trimmed_filename))
output = (encode_shell_messages(prefix, out) +
((err and encode_shell_messages(prefix, err)) or []))
return (trimmed_filename, linter_name, (returncode or 1), output)
If the result matches the error condition specified in the configuration file,
return the error code and messages, either return nothing.
"""
def run_one_linter(linter, filenames):
""" Runs one linter against a set of files
Creates a match filter for the linter, extract the files to be
linted, and runs the linter against each file, returning the
result as a list of successes and failures. Failures have a
return code and the output of the lint process.
"""
match_filter = make_match_filter([linter])
files = set([file for file in filenames if match_filter(file)])
return [run_external_linter(file, linter.linter, linter.name) for file in files]
def build_lint_runner(linters, filenames):
""" Returns a function to run a set of linters against a set of filenames
This returns a function because it's going to be wrapped in a
runner to better handle stashing and restoring a staged commit.
"""
def lint_runner():
return reduce(operator.add,
[run_one_linter(linter, filenames) for linter in linters], [])
return lint_runner
def dryrun(linters, filenames):
def dryrunonefile(filename, linter):
cmd = linter['command'] + ' "' + filename + '"'
(out, err, returncode) = get_shell_response(cmd)
failed = ((out and (linter.get('condition', 'error') == 'output')) or err or (not (returncode == 0)))
trimmed_filename = filename.replace(git_base + '/', '', 1)
return (trimmed_filename, linter.name, 0, [' {}'.format(trimmed_filename)])
def dryrunonce(linter, filenames):
match_filter = make_match_filter([linter])
files_to_check = [filename for filename in filenames if match_filter(filename)]
return [dryrunonefile(filename, linter) for filename in files_to_check]
return reduce(operator.add, [dryrunonce(linter, filenames) for linter in linters], [])
if not failed:
return (trimmed_filename, linter_name, 0, [])
prefix = (((linter.get('print', 'false').strip().lower() != 'true') and ' ')
or ' {}: '.format(trimmed_filename))
output = (encode_shell_messages(prefix, out) +
((err and encode_shell_messages(prefix, err)) or []))
return (trimmed_filename, linter_name, (returncode or 1), output)
# __ __ _
# | \/ |__ _(_)_ _
# | |\/| / _` | | ' \
# |_| |_\__,_|_|_||_|
#
def print_report(results, cmdline, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames):
sort_position = 1
grouping = 'Linter: {}'
if 'byfile' in cmdline:
sort_position = 0
grouping = 'Filename: {}'
grouped_results = group_by(results, sort_position)
for group in grouped_results:
print(grouping.format(group[0]))
for (filename, lintername, returncode, text) in group[1]:
print("\n".join(text))
print("")
if len(broken_linter_names):
print("These linters could not be run:", ",".join(broken_linter_names))
if len(cant_lint_filenames):
print("As a result, these files were not linted:")
print("\n".join([" {}".format(f) for f in cant_lint_filenames]))
if len(unlintable_filenames):
print("The following files had no recognizeable linters:")
print("\n".join([" {}".format(f) for f in unlintable_filenames]))
if len(unfindable_filenames):
print("The following files could not be found:")
print("\n".join([" {}".format(f) for f in unfindable_filenames]))
@staticmethod
def run_one_linter(linter, filenames):
""" Runs one linter against a set of files
Creates a match filter for the linter, extract the files to be
linted, and runs the linter against each file, returning the
result as a list of successes and failures. Failures have a
return code and the output of the lint process.
"""
match_filter = make_match_filter([linter])
files = set([filename for filename in filenames if match_filter(filename)])
return [self.run_external_linter(filename, linter.linter, linter.name) for filename in files]
def __call__(self, linters, filenames):
""" Returns a function to run a set of linters against a set of filenames
This returns a function because it's going to be wrapped in a
runner to better handle stashing and restoring a staged commit.
"""
return reduce(operator.add,
[run_one_linter(linter, self.filenames) for linter in self.linters], [])
def print_help(options_list, name):
print(_('Usage: {} [options] [filenames]').format(name))
for item in options_list:
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
return sys.exit()
def dryrun(self):
def dryrunonefile(filename, linter):
trimmed_filename = filename.replace(git_base + '/', '', 1)
return (trimmed_filename, linter.name, 0, [' {}'.format(trimmed_filename)])
def dryrunonce(linter, filenames):
match_filter = MatchFilter([linter])
files_to_check = [filename for filename in filenames if match_filter(filename)]
return [dryrunonefile(filename, linter) for filename in files_to_check]
return reduce(operator.add, [dryrunonce(linter, self.filenames) for linter in self.linters], [])
def print_version(name, version):
print('{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg'.format(name, version))
def run_gitlint(cmdline, config, extras):
def run_linters(options, config, extras):
def build_config_subset(keys):
""" Returns a subset of the configuration, with only those linters mentioned in keys """
return [item for item in config if item.name in keys]
""" Runs the requested linters """
all_filenames, unfindable_filenames = get_filelist(cmdline, extras)
all_filenames, unfindable_filenames = get_filelist(options, extras)
stash_runner = pick_stash_runner(cmdline)
is_lintable = make_match_filter(config)
is_lintable = MatchFilter(config)
lintable_filenames = set([filename for filename in all_filenames
if is_lintable(filename)])
@ -520,26 +467,23 @@ def run_gitlint(cmdline, config, extras):
working_linter_names, broken_linter_names = get_linter_status(config)
cant_lint_filter = make_match_filter(build_config_subset(
cant_lint_filter = MatchFilter(build_config_subset(
broken_linter_names))
cant_lint_filenames = [filename for filename in lintable_filenames
if cant_lint_filter(filename)]
if 'dryrun' in cmdline:
return print_report(
dryrun(
build_config_subset(working_linter_names), sorted(lintable_filenames)),
cmdline, unlintable_filenames, cant_lint_filenames,
if 'dryrun' in options:
dryrun_results = dryrun(
build_config_subset(working_linter_names), sorted(lintable_filenames))
return (dryrun_results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames)
runner = Runner(options)
linter = Linter(build_config_subset(working_linter_names),
sorted(lintable_filenames))
results = runner(linter, lintable_filenames)
return (results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames)
lint_runner = build_lint_runner(
build_config_subset(working_linter_names), sorted(lintable_filenames))
results = stash_runner(lint_runner, lintable_filenames)
print_report(results, cmdline, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames)
if not len(results):
return 0
return max([i[2] for i in results if len(i)])

View File

@ -6,8 +6,17 @@
# This was a lot shorter and smarter in Hy...
# A lot of what you see here is separated from git_lint itself, since this will not be
# relevant to the operation of pre-commit.
def make_rational_options(options, commandline):
# ___ _ _ _
# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___
# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_)
# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___|
#
def cleanup_options(options, commandline):
"""Takes a table of options and the commandline, and returns a
dictionary of those options that appear on the commandline
along with any extra arguments.
@ -19,7 +28,7 @@ def make_rational_options(options, commandline):
The arguments as received by the start-up process
"""
def make_options_rationalizer(options):
def make_option_streamliner(options):
"""Takes a list of option tuples, and returns a function that takes
the output of getopt and reduces it to the longopt key and
@ -28,17 +37,16 @@ def make_rational_options(options, commandline):
fullset = {}
for option in options:
if not option[1]:
continue
if option[0]:
fullset['-' + option[0]] = option[1]
fullset['--' + option[1]] = option[1]
if option[1]:
fullset['--' + option[1]] = option[1]
if option[0]:
fullset['-' + option[0]] = option[1]
def rationalizer(acc, it):
def streamliner(acc, it):
acc[fullset[it[0]]] = it[1]
return acc
return rationalizer
return streamliner
def remove_conflicted_options(options, request):
"""Takes our list of option tuples, and a cleaned copy of what was
@ -68,10 +76,10 @@ def make_rational_options(options, commandline):
optstringslong)
# Turns what getopt returns into something more human-readable
rationalize_options = make_options_rationalizer(options)
streamline_options = make_option_streamliner(options)
# Remove any options that are superseded by others.
(retoptions, excluded) = remove_conflicted_options(
optlist, reduce(rationalize_options, options, {}))
(ret, excluded) = remove_conflicted_options(
optlist, reduce(streamline_options, options, {}))
return (retoptions, filenames, excluded)
return (ret, filenames, excluded)

View File

@ -1,5 +1,7 @@
import gettext
_ = gettext.gettext
OPTIONS_LIST = [
OPTIONS = [
('o', 'only', True,
_('A comma-separated list of only those linters to run'), ['exclude']),
('x', 'exclude', True,

49
git_lint/reporters.py Normal file
View File

@ -0,0 +1,49 @@
import gettext
_ = gettext.gettext
def print_report(results, unlintable_filenames, cant_lint_filenames,
broken_linter_names, unfindable_filenames, options = {'bylinter': True}):
sort_position = 1
grouping = _('Linter: {}')
if 'byfile' in options:
sort_position = 0
grouping = _('Filename: {}')
grouped_results = group_by(results, sort_position)
for group in grouped_results:
print(grouping.format(group[0]))
for (filename, lintername, returncode, text) in group[1]:
print('\n'.join(text))
print('')
if len(broken_linter_names):
print(_('These linters could not be run:'), ','.join(broken_linter_names))
if len(cant_lint_filenames):
print(_('As a result, these files were not linted:'))
print('\n'.join([' {}'.format(f) for f in cant_lint_filenames]))
if len(unlintable_filenames):
print(_('The following files had no recognizeable linters:'))
print('\n'.join([' {}'.format(f) for f in unlintable_filenames]))
if len(unfindable_filenames):
print(_('The following files could not be found:'))
print('\n'.join([' {}'.format(f) for f in unfindable_filenames]))
def print_help(options, name):
print(_('Usage: {} [options] [filenames]').format(name))
for item in options:
print(' -{:<1} --{:<12} {}'.format(item[0], item[1], item[3]))
return sys.exit()
def print_version(name, version):
print(_'{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg').format(name, version))
def print_linters(working_linter_names, broken_linter_names):
print(_('Currently supported linters:'))
for linter in config:
print('{:<14} {}'.format(linter.name,
((linter.name in broken_linter_names and
_('(WARNING: executable not found)') or
linter.linter.get('comment', '')))))

View File

@ -23,13 +23,13 @@ test_requirements = [
]
setup(
name='git_lint',
version='0.0.2',
name='git_linter',
version='0.0.4',
description="A git command to lint everything in your workspace (or stage) that was changed since the last commit.",
long_description=readme + '\n\n' + history,
author="Kenneth M. "Elf" Sternberg",
author='Kenneth M. "Elf" Sternberg',
author_email='elf.sternberg@gmail.com',
url='https://github.com/elfsternberg/git_lint',
url='https://github.com/elfsternberg/git_linter',
packages=[
'git_lint',
],
@ -37,16 +37,20 @@ setup(
'git_lint'},
include_package_data=True,
install_requires=requirements,
license="ISCL",
license="MIT",
zip_safe=False,
keywords='git_lint',
keywords='git lint style syntaxt development',
entry_points={
'console_scripts': [
'git-lint = git_lint.__main__:main'
]
},
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: ISC License (ISCL)',
'Natural Language :: English',
"Programming Language :: Python :: 2",
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',

View File

@ -1,5 +1,5 @@
[tox]
envlist = py26, py27, py33, py34, py35
envlist = py27, py33, py34, py35
[testenv]
setenv =