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/>.
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

View file

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

View file

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

View file

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

View file

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

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)