From 24ba4d41f46ed613561012fd6f2aa8403dc712e5 Mon Sep 17 00:00:00 2001 From: "Kenneth M. Elf Sternberg" Date: Mon, 26 Sep 2016 13:56:28 -0700 Subject: [PATCH] Config is now order-preserving; switched to named tuples for config details. --- git_lint/git_lint.py | 190 +++++++++++++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 63 deletions(-) diff --git a/git_lint/git_lint.py b/git_lint/git_lint.py index 18ee109..ea16c08 100644 --- a/git_lint/git_lint.py +++ b/git_lint/git_lint.py @@ -1,5 +1,6 @@ from __future__ import print_function from functools import reduce +from collections import namedtuple import getopt import gettext import operator @@ -8,24 +9,15 @@ import re import subprocess import sys import pprint -from git_lint_options import make_rational_options -from git_lint_config import get_config +try: + import configparser +except ImportError as e: + import ConfigParser as configparser _ = gettext.gettext VERSION = '0.0.4' NAME = 'git-lint' - -def tap(a): - print("TAP:", a) - return a - -# ___ _ _ _ -# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___ -# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_) -# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___| -# - OPTIONS_LIST = [ ('o', 'only', True, _('A comma-separated list of only those linters to run'), ['exclude']), @@ -54,7 +46,14 @@ OPTIONS_LIST = [ ('h', 'help', False, _('This help message'), []), ('v', 'version', False, - _('Version information'), [])] + _('Version information'), []) +] + +# ___ _ _ _ +# / __|___ _ __ _ __ __ _ _ _ __| | | | (_)_ _ ___ +# | (__/ _ \ ' \| ' \/ _` | ' \/ _` | | |__| | ' \/ -_) +# \___\___/_|_|_|_|_|_\__,_|_||_\__,_| |____|_|_||_\___| +# # This was a lot shorter and smarter in Hy... @@ -96,7 +95,7 @@ def make_rational_options(optlist, args): the list of superseded options """ def get_excluded_keys(memo, opt): - return memo + (len(opt) > 4 and opt[4] or []) + return memo + ((len(opt) > 4 and opt[4]) or []) keys = request.keys() marked = [option for option in optlist if option[1] in keys] @@ -107,10 +106,10 @@ def make_rational_options(optlist, args): return (cleaned, excluded) def shortoptstogo(i): - return i[0] + (i[2] and ':' or '') + return i[0] + ((i[2] and ':') or '') def longoptstogo(i): - return i[1] + (i[2] and '=' or '') + return i[1] + ((i[2] and '=') or '') optstringsshort = ''.join([shortoptstogo(opt) for opt in optlist]) optstringslong = [longoptstogo(opt) for opt in optlist] @@ -126,6 +125,74 @@ def make_rational_options(optlist, args): return (retoptions, filenames, excluded) +# ___ __ _ ___ _ +# / __|___ _ _ / _(_)__ _ | _ \___ __ _ __| |___ _ _ +# | (__/ _ \ ' \| _| / _` | | / -_) _` / _` / -_) '_| +# \___\___/_||_|_| |_\__, | |_|_\___\__,_\__,_\___|_| +# |___/ + + +def find_config_file(options, base): + """ Returns the configuration file from a prioritized list of locations. + + Locations are prioritized as: + 1. From the command line. Fail if specified but not found + 2. The repository's root directory, as the file .git-lint + 3. The repository's root directory, as the file .git-lint/config + 4. The user's home directory, as file .git-lint + 5. The user's home directory, as the file .git-lint/config + + If no configuration file is found, this is an error. + """ + + if 'config' in options: + config = options['config'] + configpath = os.path.abspath(config) + if not os.path.isfile(configpath): + sys.exit(_('Configuration file not found: {}\n').format(config)) + return configpath + + home = os.path.join(os.environ.get('HOME')) + possibles = (os.path.join(base, '.git-lint'), + os.path.join(base, '.git-lint/config'), + os.path.join(home, '.git-lint'), + os.path.join(home, '.git-lint/config')) + + matches = [p for p in possibles if os.path.isfile(p)] + if len(matches) == 0: + sys.exit(_('No configuration file found')) + + return matches[0] + + +# (commandLineDictionary, repositoryLocation) -> (configurationDictionary | exit) +def get_config(options, base): + """Loads the git-lint configuration file. + + Returns the configuration file as a dictionary of dictionaries. + Performs substitutions as specified in the SafeConfigParser + specification; the only one performed currently is the 'repodir' + will be replaced with the base directory of the repository. + Combined with the option to specify the .git-lint configuration as + a directory, this allows users to keep per-project configuration + files for specific linters. + """ + + Linter = namedtuple('Linter', ['name', 'linter']) + path = find_config_file(options, base) + configloader = configparser.SafeConfigParser() + configloader.read(path) + configloader.set('DEFAULT', 'repodir', base) + return [Linter(section, {k: v for (k, v) in configloader.items(section)}) + for section in configloader.sections()] + +def ckeys(config): + return [i.name for i in config] + +def cvals(config): + return [i.linter for i in config] + + # ___ _ _ # / __(_) |_ # | (_ | | _| @@ -173,14 +240,14 @@ def get_shell_response(fullcmd): def get_git_base(): (out, error, returncode) = get_git_response_raw( ['rev-parse', '--show-toplevel']) - return returncode == 0 and out.rstrip() or None + return (returncode == 0 and out.rstrip()) or None def get_git_head(): empty_repository_hash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' (out, err, returncode) = get_git_response_raw( ['rev-parse', '--verify HEAD']) - return (err and empty_repository_hash or 'HEAD') + return ((err and empty_repository_hash) or 'HEAD') git_base = get_git_base() @@ -194,7 +261,7 @@ git_head = get_git_head() # def base_file_cleaner(files): - return [file.replace(git_base, '', 1) for file in files] + return [file.replace(git_base + '/', '', 1) for file in files] def make_match_filter_matcher(extensions): @@ -205,8 +272,7 @@ def make_match_filter_matcher(extensions): def make_match_filter(config): - matcher = make_match_filter_matcher([v.get('match', '') - for v in config.values()]) + matcher = make_match_filter_matcher([v.linter.get('match', '') for v in config]) def match_filter(path): return matcher.search(path) @@ -234,31 +300,29 @@ def executable_exists(script, label): return os.path.exists(path) and os.access(path, os.X_OK) if scriptname.startswith('/'): - return is_executable(scriptname) and scriptname or None + return (is_executable(scriptname) and scriptname) or None possibles = [path for path in [os.path.join(path, scriptname) for path in os.environ.get('PATH').split(':')] if is_executable(path)] - return len(possibles) and possibles.pop(0) or None + return (len(possibles) and possibles.pop(0)) or None -def get_working_linters(config): - return set([key for key in config.keys() - if executable_exists(config[key]['command'], key)]) +def get_working_linter_names(config): + return [i.name for i in config + if executable_exists(i.linter['command'], i.name)] def print_linters(config): print(_('Currently supported linters:')) - working = get_working_linters(config) - broken = (set(config.keys()) - working) - for key in sorted(working): - print('{:<14} {}'.format(key, config[key].get('comment', ''))) - for key in sorted(broken): - print('{:<14} {}'.format(key, _('(WARNING: executable not found)'))) - - - + working = get_working_linter_names(config) + broken = set([i.name for i in config]) - set(working) + for linter in config: + print('{:<14} {}'.format(linter.name, + ((linter.name in broken and + _('(WARNING: executable not found)') or + linter.linter.get('comment', ''))))) # ___ _ _ _ _ __ __ _ _ @@ -354,11 +418,9 @@ def get_filelist(cmdline, extras): if len(extras): cwd = os.path.abspath(os.getcwd()) - extras_fullpathed = set([os.path.join(cwd, f) for f in extras]) + extras_fullpathed = set([os.path.abspath(os.path.join(cwd, f)) for f in extras]) not_found = set([f for f in extras_fullpathed if not os.path.isfile(f)]) - for f in not_found: - print(_("File not found: {}").format(f)) - return [os.path.relpath(f, cwd) for f in (extras_fullpathed - not_found)] + 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: @@ -370,7 +432,7 @@ def get_filelist(cmdline, extras): if 'staging' in keys: file_list_generator = staging_list - return working_directory_trans(remove_submodules(file_list_generator())) + return (working_directory_trans(remove_submodules(file_list_generator())), []) # ___ _ _ @@ -409,7 +471,7 @@ def pick_stash_runner(cmdline): def workspace_wrapper(run_linters): return run_linters() - return 'staging' in cmdline and staging_wrapper or workspace_wrapper + return ('staging' in cmdline and staging_wrapper) or workspace_wrapper # ___ _ _ _ # | _ \_ _ _ _ | (_)_ _| |_ _ __ __ _ ______ @@ -437,8 +499,8 @@ def run_external_linter(filename, linter, linter_name): if not failed: return (filename, linter_name, 0, []) - prefix = linter.get('print', False) and '\t{}:'.format(filename) or '\t' - output = encode_shell_messages(prefix, out) + (err and encode_shell_messages(prefix, err) or []) + prefix = (linter.get('print', False) and '\t{}: '.format(filename)) or '\t' + output = encode_shell_messages(prefix, out) + ((err and encode_shell_messages(prefix, err)) or []) return (filename, linter_name, (returncode or 1), output) @@ -483,36 +545,38 @@ def run_gitlint(cmdline, config, extras): def build_config_subset(keys): """ Returns a subset of the configuration, with only those linters mentioned in keys """ - return {item[0]: item[1] for item in config.items() if item[0] in keys} + return [item for item in config if item.name in keys] """ Runs the requested linters """ - all_files = get_filelist(cmdline, extras) - stash_runner = pick_runner(cmdline) - is_lintable = make_match_filter(config) - lintable_files = set([file for file in all_files if lintable(file)]) - unlintable_files = (set(all_files) - lintable_files) + (all_filenames, unfindable_filenames) = get_filelist(cmdline, extras) + + stash_runner = pick_stash_runner(cmdline) + is_lintable = make_match_filter(config) + lintable_filenames = set([filename for filename in all_filenames if is_lintable(filename)]) + unlintable_filenames = set(all_filenames) - lintable_filenames + + working_linter_names = get_working_linter_names(config) + broken_linter_names = (set([i.name for i in config]) - set(working_linter_names)) + cant_lint_filter = make_match_filter(build_config_subset(broken_linter_names)) + cant_lint_filenames = [filename for filename in lintable_filenames if cant_lint_filter(filename)] - working_linters = get_working_linters(config) - broken_linters = (set(config) - set(working_linters)) - cant_lint_filter = make_match_filter(build_config_subset(broken_linters)) - cant_lint_files = set([file for file in lintable_files if cant_lint_filter(file)]) lint_runner = build_lint_runner(build_config_subset(working_linters), lintable_files) results = stash_runner(lint_runner) print(list(results)) - return max(*[i[2] for i in results if len(i)]) + return max([i[2] for i in results if len(i)]) -def print_help(): - print(_('Usage: {} [options] [filenames]').format(NAME)) - for item in OPTIONS_LIST: +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 print_version(): - print('{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg'.format(NAME, VERSION)) +def print_version(name, version): + print('{} {} Copyright (c) 2009, 2016 Kennth M. "Elf" Sternberg'.format(name, version)) def main(*args): @@ -524,17 +588,17 @@ def main(*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) + print("\t{}".format(exc)) try: config = get_config(cmdline, git_base) if 'help' in cmdline: - print_help() + print_help(OPTIONS_LIST, NAME) return 0 if 'version' in cmdline: - print_version() + print_version(NAME, VERSION) return 0 if 'linters' in cmdline: