From 0d2bf9b0a8c1ae42576455ce58eac71d8eaccc8e Mon Sep 17 00:00:00 2001 From: Adam Waldenberg Date: Tue, 9 Jul 2013 12:40:59 +0200 Subject: [PATCH] Implemented optional boolean arguments to some command-line options. Just like in many GNU tools, it is now possible to pass an optional boolean to some of the flags of gitinspector in the form; --flag[=BOOL] This gives us the ability to override options set via git-config. For example; say we did the following: git-config --global inspector.timeline true We could then override this setting when running gitinspector by supplying: ./gitinspector.py --timeline=false Implementing this was not a trivial task, as no command-line parser in Python supports this by default (getopt, optparse, argparse). In order to properly handle optional boolean arguments; some clever patching had to be done to the command-line in combination with a callback function that can handle boolean strings. To maintain compatibility with Python 2.6, this was implemented using optparse (instead of argparse). --- gitinspector/config.py | 84 ++++++++-------------- gitinspector/extensions.py | 5 +- gitinspector/format.py | 5 +- gitinspector/gitinspector.py | 136 ++++++++++++++++++----------------- gitinspector/help.py | 70 +++++++++--------- gitinspector/optval.py | 61 ++++++++++++++++ 6 files changed, 202 insertions(+), 159 deletions(-) create mode 100644 gitinspector/optval.py diff --git a/gitinspector/config.py b/gitinspector/config.py index d510390..ad5f8af 100644 --- a/gitinspector/config.py +++ b/gitinspector/config.py @@ -18,17 +18,15 @@ # along with gitinspector. If not, see . from __future__ import unicode_literals -import extensions -import filtering -import format -import interval -import missing import os import subprocess -def __read_git_config__(repo, variable, default_value): +def __read_git_config__(run, variable, destination=None): + if destination == None: + destination = variable + previous_directory = os.getcwd() - os.chdir(repo) + os.chdir(run.repo) setting = subprocess.Popen("git config inspector." + variable, shell=True, bufsize=1, stdout=subprocess.PIPE).stdout os.chdir(previous_directory) @@ -37,55 +35,33 @@ def __read_git_config__(repo, variable, default_value): setting = setting.readlines()[0] setting = setting.decode("utf-8", "replace").strip() - if default_value == True or default_value == False: - if setting == "True" or setting == "true" or setting == "1": - return True - elif setting == "False" or setting == "false" or setting == "0": - return False - - return False - elif setting == "": - return default_value - - return setting + if setting == "True" or setting == "true" or setting == "t" or setting == "1": + run.opts[destination] = True + elif setting == "False" or setting == "false" or setting == "f" or setting == "0": + run.opts[destination] = False + return True except IndexError: - return default_value + return False def init(run): - missing.set_checkout_missing(__read_git_config__(run.repo, "checkout-missing", False)) - extensions.define(__read_git_config__(run.repo, "file-types", ",".join(extensions.get()))) + __read_git_config__(run, "checkout-missing", "checkout_missing") + __read_git_config__(run, "file-types", "file_types") + __read_git_config__(run, "exclude") + __read_git_config__(run, "format") + __read_git_config__(run, "hard") + __read_git_config__(run, "list-file-types", "list_file_types") + __read_git_config__(run, "metrics") + __read_git_config__(run, "responsibilities") + __read_git_config__(run, "weeks", "useweeks") + __read_git_config__(run, "since") + __read_git_config__(run, "until") + __read_git_config__(run, "timeline") - exclude = __read_git_config__(run.repo, "exclude", None) - if exclude != None: - filtering.add(exclude) - - output_format = __read_git_config__(run.repo, "format", None) - if output_format != None: - if not format.select(output_format): - raise format.InvalidFormatError(_("specified output format not supported.")) - - run.hard = __read_git_config__(run.repo, "hard", False) - run.list_file_types = __read_git_config__(run.repo, "list-file-types", False) - run.include_metrics = __read_git_config__(run.repo, "metrics", False) - run.responsibilities = __read_git_config__(run.repo, "responsibilities", False) - run.useweeks = __read_git_config__(run.repo, "weeks", False) - - since = __read_git_config__(run.repo, "since", None) - if since != None: - interval.set_since(since) - - until = __read_git_config__(run.repo, "until", None) - if until != None: - interval.set_until(until) - - run.timeline = __read_git_config__(run.repo, "timeline", False) - - if __read_git_config__(run.repo, "grading", False): - run.include_metrics = True - run.list_file_types = True - run.responsibilities = True - run.grading = True - run.hard = True - run.timeline = True - run.useweeks = True + if __read_git_config__(run, "grading"): + run.opts.hard = True + run.opts.list_file_types = True + run.opts.metrics = True + run.opts.responsibilities = True + run.opts.timeline = True + run.opts.useweeks = True diff --git a/gitinspector/extensions.py b/gitinspector/extensions.py index bffa6fa..43f266e 100644 --- a/gitinspector/extensions.py +++ b/gitinspector/extensions.py @@ -23,8 +23,9 @@ from outputable import Outputable import terminal import textwrap -__default_extensions__ = ["java", "c", "cpp", "h", "hpp", "py", "glsl", "rb", "js", "sql"] -__extensions__ = __default_extensions__ +DEFAULT_EXTENSIONS = ["java", "c", "cpp", "h", "hpp", "py", "glsl", "rb", "js", "sql"] + +__extensions__ = None __located_extensions__ = set() def get(): diff --git a/gitinspector/format.py b/gitinspector/format.py index 855acc4..f327fe0 100644 --- a/gitinspector/format.py +++ b/gitinspector/format.py @@ -26,8 +26,9 @@ import os import zipfile __available_formats__ = ["html", "htmlembedded", "text", "xml"] -__default_format__ = __available_formats__[2] -__selected_format__ = __default_format__ +__selected_format__ = None + +DEFAULT_FORMAT = __available_formats__[2] class InvalidFormatError(Exception): pass diff --git a/gitinspector/gitinspector.py b/gitinspector/gitinspector.py index 4809e1d..8537f51 100755 --- a/gitinspector/gitinspector.py +++ b/gitinspector/gitinspector.py @@ -30,12 +30,13 @@ import config import extensions import filtering import format -import getopt import help import interval import metrics import missing import os +import optparse +import optval import outputable import responsibilities import sys @@ -44,40 +45,50 @@ import timeline import version class Runner: - def __init__(self): - self.hard = False - self.include_metrics = False - self.list_file_types = False + def __init__(self, opts): + self.opts = opts self.repo = "." - self.responsibilities = False - self.grading = False - self.timeline = False - self.useweeks = False def output(self): terminal.skip_escapes(not sys.stdout.isatty()) terminal.set_stdout_encoding() previous_directory = os.getcwd() os.chdir(self.repo) + + if not format.select(self.opts.format): + raise format.InvalidFormatError(_("specified output format not supported.")) + + missing.set_checkout_missing(self.opts.checkout_missing) + extensions.define(self.opts.file_types) + + if self.opts.since != None: + interval.set_since(self.opts.since) + + if self.opts.until != None: + interval.set_until(self.opts.until) + + for ex in self.opts.exclude: + filtering.add(ex) + format.output_header() - outputable.output(changes.ChangesOutput(self.hard)) + outputable.output(changes.ChangesOutput(self.opts.hard)) - if changes.get(self.hard).get_commits(): - outputable.output(blame.BlameOutput(self.hard)) + if changes.get(self.opts.hard).get_commits(): + outputable.output(blame.BlameOutput(self.opts.hard)) - if self.timeline: - outputable.output(timeline.Timeline(changes.get(self.hard), self.useweeks)) + if self.opts.timeline: + outputable.output(timeline.Timeline(changes.get(self.opts.hard), self.opts.useweeks)) - if self.include_metrics: + if self.opts.metrics: outputable.output(metrics.Metrics()) - if self.responsibilities: - outputable.output(responsibilities.ResponsibilitiesOutput(self.hard)) + if self.opts.responsibilities: + outputable.output(responsibilities.ResponsibilitiesOutput(self.opts.hard)) outputable.output(missing.Missing()) outputable.output(filtering.Filtering()) - if self.list_file_types: + if self.opts.list_file_types: outputable.output(extensions.Extensions()) format.output_footer() @@ -88,63 +99,54 @@ def __check_python_version__(): python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) sys.exit(_("gitinspector requires at leat Python 2.6 to run (version {0} was found).").format(python_version)) +def __handle_help__(__option__, __opt_str__, __value__, __parser__): + help.output() + sys.exit(0) + +def __handle_version__(__option__, __opt_str__, __value__, __parser__): + version.output() + sys.exit(0) + def main(): - __run__ = Runner() + parser = optparse.OptionParser(add_help_option=False) try: - __opts__, __args__ = getopt.gnu_getopt(sys.argv[1:], "cf:F:hHlmrTwx:", ["checkout-missing", "exclude=", - "file-types=", "format=", "hard", "help", "list-file-types", - "metrics", "responsibilities", "since=", "grading", - "timeline", "until=", "version", "weeks"]) - for arg in __args__: + parser.add_option("-c", action="store_true", dest="checkout_missing") + parser.add_option("-H", action="store_true", dest="hard") + parser.add_option("-l", action="store_true", dest="list_file_types") + parser.add_option("-m", action="store_true", dest="metrics") + parser.add_option("-r", action="store_true", dest="responsibilities") + parser.add_option("-T", action="store_true", dest="timeline") + parser.add_option("-w", action="store_true", dest="useweeks") + + optval.add_option(parser, "--checkout-missing", boolean=True) + parser.add_option( "-f", "--file-types", type="string", default=",".join(extensions.DEFAULT_EXTENSIONS)) + parser.add_option( "-F", "--format", type="string", default=format.DEFAULT_FORMAT) + optval.add_option(parser, "--grading", boolean=True, multidest=["hard", "metrics", "list_file_types", + "responsibilities", "timeline", "useweeks"]) + parser.add_option( "-h", "--help", action="callback", callback=__handle_help__) + optval.add_option(parser, "--hard", boolean=True) + optval.add_option(parser, "--list-file-types", boolean=True) + optval.add_option(parser, "--metrics", boolean=True) + optval.add_option(parser, "--responsibilities", boolean=True) + parser.add_option( "--since", type="string") + optval.add_option(parser, "--timeline", boolean=True) + parser.add_option( "--until", type="string") + parser.add_option( "--version", action="callback", callback=__handle_version__) + optval.add_option(parser, "--weeks", boolean=True, dest="useweeks") + parser.add_option( "-x", "--exclude", action="append", type="string", default=[]) + + (opts, args) = parser.parse_args() + __run__ = Runner(opts) + + for arg in args: __run__.repo = arg #We need the repo above to be set before we read the git config. config.init(__run__) - for o, a in __opts__: - if o in("-c", "--checkout-missing"): - missing.set_checkout_missing(True) - elif o in("-h", "--help"): - help.output() - sys.exit(0) - elif o in("-f", "--file-types"): - extensions.define(a) - elif o in("-F", "--format"): - if not format.select(a): - raise format.InvalidFormatError(_("specified output format not supported.")) - elif o in("-H", "--hard"): - __run__.hard = True - elif o in("-l", "--list-file-types"): - __run__.list_file_types = True - elif o in("-m", "--metrics"): - __run__.include_metrics = True - elif o in("-r", "--responsibilities"): - __run__.responsibilities = True - elif o in("--since"): - interval.set_since(a) - elif o in("--version"): - version.output() - sys.exit(0) - elif o in("--grading"): - __run__.include_metrics = True - __run__.list_file_types = True - __run__.responsibilities = True - __run__.grading = True - __run__.hard = True - __run__.timeline = True - __run__.useweeks = True - elif o in("-T", "--timeline"): - __run__.timeline = True - elif o in("--until"): - interval.set_until(a) - elif o in("-w", "--weeks"): - __run__.useweeks = True - elif o in("-x", "--exclude"): - filtering.add(a) - - except (format.InvalidFormatError, getopt.error) as msg: - print(sys.argv[0], "\b:", msg) + except (format.InvalidFormatError, optval.InvalidOptionArgument) as msg: + print(sys.argv[0], "\b:", unicode(msg)) print(_("Try `{0} --help' for more information.").format(sys.argv[0])) sys.exit(2) diff --git a/gitinspector/help.py b/gitinspector/help.py index 6301aca..f923b82 100644 --- a/gitinspector/help.py +++ b/gitinspector/help.py @@ -19,7 +19,7 @@ from __future__ import print_function from __future__ import unicode_literals -from extensions import __default_extensions__ +from extensions import DEFAULT_EXTENSIONS from format import __available_formats__ import sys @@ -29,38 +29,40 @@ specified, the current directory is used. If multiple directories are given, information will be fetched from the last directory specified. Mandatory arguments to long options are mandatory for short options too. - -c, --checkout-missing try to checkout any missing files - -f, --file-types=EXTENSIONS a comma separated list of file extensions to - include when computing statistics. The - default extensions used are: - {1} - -F, --format=FORMAT define in which format output should be - generated; the default format is 'text' and - the available formats are: - {2} - --grading show statistics and information in a way that - is formatted for grading of student projects; - this is the same as supplying -HlmrTw - -H, --hard track rows and look for duplicates harder; - this can be quite slow with big repositories - -l, --list-file-types list all the file extensions available in the - current branch of the repository - -m --metrics include checks for certain metrics during the - analysis of commits - -r --responsibilities show which files the different authors seem - most responsible for - --since=DATE only show statistics for commits more recent - than a specific date - -T, --timeline show commit timeline, including author names - --until=DATE only show statistics for commits older than a - specific date - -w, --weeks show all statistical information in weeks - instead of in months - -x, --exclude=PATTERN an exclusion pattern describing file names that - should be excluded from the statistics; can - be specified multiple times - -h, --help display this help and exit - --version output version information and exit +Boolean arguments can only be given to long options. + -c, --checkout-missing[=BOOL] try to checkout any missing files + -f, --file-types=EXTENSIONS a comma separated list of file extensions to + include when computing statistics. The + default extensions used are: + {1} + -F, --format=FORMAT define in which format output should be + generated; the default format is 'text' and + the available formats are: + {2} + --grading[=BOOL] show statistics and information in a way that + is formatted for grading of student + projects; this is the same as supplying the + options -HlmrTw + -H, --hard[=BOOL] track rows and look for duplicates harder; + this can be quite slow with big repositories + -l, --list-file-types[=BOOL] list all the file extensions available in the + current branch of the repository + -m --metrics[=BOOL] include checks for certain metrics during the + analysis of commits + -r --responsibilities[=BOOL] show which files the different authors seem + most responsible for + --since=DATE only show statistics for commits more recent + than a specific date + -T, --timeline[=BOOL] show commit timeline, including author names + --until=DATE only show statistics for commits older than a + specific date + -w, --weeks[=BOOL] show all statistical information in weeks + instead of in months + -x, --exclude=PATTERN an exclusion pattern describing file names + that should be excluded from the statistics; + can be specified multiple times + -h, --help display this help and exit + --version output version information and exit gitinspector will filter statistics to only include commits that modify, add or remove one of the specified extensions, see -f or --file-types for @@ -70,4 +72,4 @@ gitinspector requires that the git executable is available in your PATH. Report gitinspector bugs to gitinspector@ejwa.se.""") def output(): - print(__doc__.format(sys.argv[0], ",".join(__default_extensions__), ",".join(__available_formats__))) + print(__doc__.format(sys.argv[0], ",".join(DEFAULT_EXTENSIONS), ",".join(__available_formats__))) diff --git a/gitinspector/optval.py b/gitinspector/optval.py new file mode 100644 index 0000000..3876f59 --- /dev/null +++ b/gitinspector/optval.py @@ -0,0 +1,61 @@ +# coding: utf-8 +# +# Copyright © 2013 Ejwa Software. All rights reserved. +# +# This file is part of gitinspector. +# +# gitinspector 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. +# +# gitinspector 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 + +from __future__ import unicode_literals + +import sys + +class InvalidOptionArgument(Exception): + pass + +def __handle_boolean_argument__(option, __opt_str__, value, parser, *__args__, **kwargs): + if isinstance(value, bool): + return value + elif value == None or value.lower() == "false" or value.lower() == "f" or value == "0": + value = False + elif value.lower() == "true" or value.lower() == "t" or value == "1": + value = True + else: + raise InvalidOptionArgument("The given option argument is not a valid boolean.") + + if "multidest" in kwargs: + for dest in kwargs["multidest"]: + setattr(parser.values, dest, value) + + setattr(parser.values, option.dest, value) + +# Originaly taken from here (and modified): +# http://stackoverflow.com/questions/1229146/parsing-empty-options-in-python + +def add_option(parser, *args, **kwargs): + if "multidest" in kwargs: + multidest = kwargs.pop("multidest") + kwargs["callback_kwargs"] = {"multidest": multidest} + if "boolean" in kwargs and kwargs["boolean"] == True: + boolean = kwargs.pop("boolean") + kwargs["type"] = "string" + kwargs["action"] = "callback" + kwargs["callback"] = __handle_boolean_argument__ + kwargs["default"] = not boolean + + for i in range(len(sys.argv) - 1, 0, -1): + arg = sys.argv[i] + if arg in args: + sys.argv.insert(i + 1, "true") + + parser.add_option(*args, **kwargs)