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).
This commit is contained in:
Adam Waldenberg 2013-07-09 12:40:59 +02:00
parent 663493fd41
commit 0d2bf9b0a8
6 changed files with 202 additions and 159 deletions

View File

@ -18,17 +18,15 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals from __future__ import unicode_literals
import extensions
import filtering
import format
import interval
import missing
import os import os
import subprocess 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() previous_directory = os.getcwd()
os.chdir(repo) os.chdir(run.repo)
setting = subprocess.Popen("git config inspector." + variable, shell=True, bufsize=1, setting = subprocess.Popen("git config inspector." + variable, shell=True, bufsize=1,
stdout=subprocess.PIPE).stdout stdout=subprocess.PIPE).stdout
os.chdir(previous_directory) os.chdir(previous_directory)
@ -37,55 +35,33 @@ def __read_git_config__(repo, variable, default_value):
setting = setting.readlines()[0] setting = setting.readlines()[0]
setting = setting.decode("utf-8", "replace").strip() setting = setting.decode("utf-8", "replace").strip()
if default_value == True or default_value == False: if setting == "True" or setting == "true" or setting == "t" or setting == "1":
if setting == "True" or setting == "true" or setting == "1": run.opts[destination] = True
return True elif setting == "False" or setting == "false" or setting == "f" or setting == "0":
elif setting == "False" or setting == "false" or setting == "0": run.opts[destination] = False
return False return True
return False
elif setting == "":
return default_value
return setting
except IndexError: except IndexError:
return default_value return False
def init(run): def init(run):
missing.set_checkout_missing(__read_git_config__(run.repo, "checkout-missing", False)) __read_git_config__(run, "checkout-missing", "checkout_missing")
extensions.define(__read_git_config__(run.repo, "file-types", ",".join(extensions.get()))) __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 __read_git_config__(run, "grading"):
if exclude != None: run.opts.hard = True
filtering.add(exclude) run.opts.list_file_types = True
run.opts.metrics = True
output_format = __read_git_config__(run.repo, "format", None) run.opts.responsibilities = True
if output_format != None: run.opts.timeline = True
if not format.select(output_format): run.opts.useweeks = True
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

View File

@ -23,8 +23,9 @@ from outputable import Outputable
import terminal import terminal
import textwrap import textwrap
__default_extensions__ = ["java", "c", "cpp", "h", "hpp", "py", "glsl", "rb", "js", "sql"] DEFAULT_EXTENSIONS = ["java", "c", "cpp", "h", "hpp", "py", "glsl", "rb", "js", "sql"]
__extensions__ = __default_extensions__
__extensions__ = None
__located_extensions__ = set() __located_extensions__ = set()
def get(): def get():

View File

@ -26,8 +26,9 @@ import os
import zipfile import zipfile
__available_formats__ = ["html", "htmlembedded", "text", "xml"] __available_formats__ = ["html", "htmlembedded", "text", "xml"]
__default_format__ = __available_formats__[2] __selected_format__ = None
__selected_format__ = __default_format__
DEFAULT_FORMAT = __available_formats__[2]
class InvalidFormatError(Exception): class InvalidFormatError(Exception):
pass pass

View File

@ -30,12 +30,13 @@ import config
import extensions import extensions
import filtering import filtering
import format import format
import getopt
import help import help
import interval import interval
import metrics import metrics
import missing import missing
import os import os
import optparse
import optval
import outputable import outputable
import responsibilities import responsibilities
import sys import sys
@ -44,40 +45,50 @@ import timeline
import version import version
class Runner: class Runner:
def __init__(self): def __init__(self, opts):
self.hard = False self.opts = opts
self.include_metrics = False
self.list_file_types = False
self.repo = "." self.repo = "."
self.responsibilities = False
self.grading = False
self.timeline = False
self.useweeks = False
def output(self): def output(self):
terminal.skip_escapes(not sys.stdout.isatty()) terminal.skip_escapes(not sys.stdout.isatty())
terminal.set_stdout_encoding() terminal.set_stdout_encoding()
previous_directory = os.getcwd() previous_directory = os.getcwd()
os.chdir(self.repo) 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() format.output_header()
outputable.output(changes.ChangesOutput(self.hard)) outputable.output(changes.ChangesOutput(self.opts.hard))
if changes.get(self.hard).get_commits(): if changes.get(self.opts.hard).get_commits():
outputable.output(blame.BlameOutput(self.hard)) outputable.output(blame.BlameOutput(self.opts.hard))
if self.timeline: if self.opts.timeline:
outputable.output(timeline.Timeline(changes.get(self.hard), self.useweeks)) outputable.output(timeline.Timeline(changes.get(self.opts.hard), self.opts.useweeks))
if self.include_metrics: if self.opts.metrics:
outputable.output(metrics.Metrics()) outputable.output(metrics.Metrics())
if self.responsibilities: if self.opts.responsibilities:
outputable.output(responsibilities.ResponsibilitiesOutput(self.hard)) outputable.output(responsibilities.ResponsibilitiesOutput(self.opts.hard))
outputable.output(missing.Missing()) outputable.output(missing.Missing())
outputable.output(filtering.Filtering()) outputable.output(filtering.Filtering())
if self.list_file_types: if self.opts.list_file_types:
outputable.output(extensions.Extensions()) outputable.output(extensions.Extensions())
format.output_footer() format.output_footer()
@ -88,63 +99,54 @@ def __check_python_version__():
python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) 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)) 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(): def main():
__run__ = Runner() parser = optparse.OptionParser(add_help_option=False)
try: try:
__opts__, __args__ = getopt.gnu_getopt(sys.argv[1:], "cf:F:hHlmrTwx:", ["checkout-missing", "exclude=", parser.add_option("-c", action="store_true", dest="checkout_missing")
"file-types=", "format=", "hard", "help", "list-file-types", parser.add_option("-H", action="store_true", dest="hard")
"metrics", "responsibilities", "since=", "grading", parser.add_option("-l", action="store_true", dest="list_file_types")
"timeline", "until=", "version", "weeks"]) parser.add_option("-m", action="store_true", dest="metrics")
for arg in __args__: 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 __run__.repo = arg
#We need the repo above to be set before we read the git config. #We need the repo above to be set before we read the git config.
config.init(__run__) config.init(__run__)
for o, a in __opts__: except (format.InvalidFormatError, optval.InvalidOptionArgument) as msg:
if o in("-c", "--checkout-missing"): print(sys.argv[0], "\b:", unicode(msg))
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)
print(_("Try `{0} --help' for more information.").format(sys.argv[0])) print(_("Try `{0} --help' for more information.").format(sys.argv[0]))
sys.exit(2) sys.exit(2)

View File

@ -19,7 +19,7 @@
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from extensions import __default_extensions__ from extensions import DEFAULT_EXTENSIONS
from format import __available_formats__ from format import __available_formats__
import sys 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. given, information will be fetched from the last directory specified.
Mandatory arguments to long options are mandatory for short options too. Mandatory arguments to long options are mandatory for short options too.
-c, --checkout-missing try to checkout any missing files Boolean arguments can only be given to long options.
-f, --file-types=EXTENSIONS a comma separated list of file extensions to -c, --checkout-missing[=BOOL] try to checkout any missing files
include when computing statistics. The -f, --file-types=EXTENSIONS a comma separated list of file extensions to
default extensions used are: include when computing statistics. The
{1} default extensions used are:
-F, --format=FORMAT define in which format output should be {1}
generated; the default format is 'text' and -F, --format=FORMAT define in which format output should be
the available formats are: generated; the default format is 'text' and
{2} the available formats are:
--grading show statistics and information in a way that {2}
is formatted for grading of student projects; --grading[=BOOL] show statistics and information in a way that
this is the same as supplying -HlmrTw is formatted for grading of student
-H, --hard track rows and look for duplicates harder; projects; this is the same as supplying the
this can be quite slow with big repositories options -HlmrTw
-l, --list-file-types list all the file extensions available in the -H, --hard[=BOOL] track rows and look for duplicates harder;
current branch of the repository this can be quite slow with big repositories
-m --metrics include checks for certain metrics during the -l, --list-file-types[=BOOL] list all the file extensions available in the
analysis of commits current branch of the repository
-r --responsibilities show which files the different authors seem -m --metrics[=BOOL] include checks for certain metrics during the
most responsible for analysis of commits
--since=DATE only show statistics for commits more recent -r --responsibilities[=BOOL] show which files the different authors seem
than a specific date most responsible for
-T, --timeline show commit timeline, including author names --since=DATE only show statistics for commits more recent
--until=DATE only show statistics for commits older than a than a specific date
specific date -T, --timeline[=BOOL] show commit timeline, including author names
-w, --weeks show all statistical information in weeks --until=DATE only show statistics for commits older than a
instead of in months specific date
-x, --exclude=PATTERN an exclusion pattern describing file names that -w, --weeks[=BOOL] show all statistical information in weeks
should be excluded from the statistics; can instead of in months
be specified multiple times -x, --exclude=PATTERN an exclusion pattern describing file names
-h, --help display this help and exit that should be excluded from the statistics;
--version output version information and exit 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, gitinspector will filter statistics to only include commits that modify,
add or remove one of the specified extensions, see -f or --file-types for 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.""") Report gitinspector bugs to gitinspector@ejwa.se.""")
def output(): 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__)))

61
gitinspector/optval.py Normal file
View File

@ -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)