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
elif setting == "False" or setting == "false" or setting == "f" or setting == "0":
run.opts[destination] = False
return True return True
elif setting == "False" or setting == "false" or setting == "0":
return False
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,7 +29,8 @@ 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.
-c, --checkout-missing[=BOOL] try to checkout any missing files
-f, --file-types=EXTENSIONS a comma separated list of file extensions to -f, --file-types=EXTENSIONS a comma separated list of file extensions to
include when computing statistics. The include when computing statistics. The
default extensions used are: default extensions used are:
@ -38,27 +39,28 @@ Mandatory arguments to long options are mandatory for short options too.
generated; the default format is 'text' and generated; the default format is 'text' and
the available formats are: the available formats are:
{2} {2}
--grading show statistics and information in a way that --grading[=BOOL] show statistics and information in a way that
is formatted for grading of student projects; is formatted for grading of student
this is the same as supplying -HlmrTw projects; this is the same as supplying the
-H, --hard track rows and look for duplicates harder; options -HlmrTw
-H, --hard[=BOOL] track rows and look for duplicates harder;
this can be quite slow with big repositories this can be quite slow with big repositories
-l, --list-file-types list all the file extensions available in the -l, --list-file-types[=BOOL] list all the file extensions available in the
current branch of the repository current branch of the repository
-m --metrics include checks for certain metrics during the -m --metrics[=BOOL] include checks for certain metrics during the
analysis of commits analysis of commits
-r --responsibilities show which files the different authors seem -r --responsibilities[=BOOL] show which files the different authors seem
most responsible for most responsible for
--since=DATE only show statistics for commits more recent --since=DATE only show statistics for commits more recent
than a specific date than a specific date
-T, --timeline show commit timeline, including author names -T, --timeline[=BOOL] show commit timeline, including author names
--until=DATE only show statistics for commits older than a --until=DATE only show statistics for commits older than a
specific date specific date
-w, --weeks show all statistical information in weeks -w, --weeks[=BOOL] show all statistical information in weeks
instead of in months instead of in months
-x, --exclude=PATTERN an exclusion pattern describing file names that -x, --exclude=PATTERN an exclusion pattern describing file names
should be excluded from the statistics; can that should be excluded from the statistics;
be specified multiple times can be specified multiple times
-h, --help display this help and exit -h, --help display this help and exit
--version output version information and exit --version output version information and exit
@ -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)