From ce7591bee38eb3b94f9b68cb5699a597195ff8fa Mon Sep 17 00:00:00 2001 From: Pavel Raiskup Date: Sun, 20 Jan 2013 23:20:50 +0100 Subject: [PATCH] Downstream patch to follow https://github.com/praiskup/pybugz --- bin/bugz | 19 ++--- bugz/argparsers.py | 10 ++- bugz/cli.py | 209 ++++++++++++++++++++++++++++------------------ bugz/configfile.py | 217 +++++++++++++++++++++++++++++++++--------------- bugz/errhandling.py | 6 ++ bugz/log.py | 72 ++++++++++++++++ bugzrc.example | 61 -------------- conf/conf.d/gentoo.conf | 3 + conf/conf.d/redhat.conf | 3 + conf/pybugz.conf | 126 ++++++++++++++++++++++++++++ man/bugz.1 | 18 ++-- setup.py | 4 + 12 files changed, 520 insertions(+), 228 deletions(-) create mode 100644 bugz/errhandling.py create mode 100644 bugz/log.py delete mode 100644 bugzrc.example create mode 100644 conf/conf.d/gentoo.conf create mode 100644 conf/conf.d/redhat.conf create mode 100644 conf/pybugz.conf diff --git a/bin/bugz b/bin/bugz index 4e61ddf..6f72fa1 100755 --- a/bin/bugz +++ b/bin/bugz @@ -25,17 +25,16 @@ import sys import traceback from bugz.argparsers import make_parser -from bugz.cli import BugzError, PrettyBugz -from bugz.configfile import get_config +from bugz.cli import PrettyBugz +from bugz.errhandling import BugzError +from bugz.log import * def main(): - parser = make_parser() # parse options + args = None + parser = make_parser() args = parser.parse_args() - get_config(args) - if getattr(args, 'columns') is None: - setattr(args, 'columns', 0) try: bugz = PrettyBugz(args) @@ -43,17 +42,17 @@ def main(): return 0 except BugzError, e: - print ' ! Error: %s' % e + log_error(e) return 1 except TypeError, e: - print ' ! Error: Incorrect number of arguments supplied' - print + # where this comes from? + log_error('Incorrect number of arguments supplied') traceback.print_exc() return 1 except RuntimeError, e: - print ' ! Error: %s' % e + log_error(e) return 1 except KeyboardInterrupt: diff --git a/bugz/argparsers.py b/bugz/argparsers.py index d14dd84..4cf3936 100644 --- a/bugz/argparsers.py +++ b/bugz/argparsers.py @@ -258,11 +258,12 @@ def make_parser(): parser = argparse.ArgumentParser( epilog = 'use -h after a sub-command for sub-command specific help') parser.add_argument('--config-file', + default = None, help = 'read an alternate configuration file') parser.add_argument('--connection', help = 'use [connection] section of your configuration file') parser.add_argument('-b', '--base', - default = 'https://bugs.gentoo.org/xmlrpc.cgi', + default = None, help = 'base URL of Bugzilla') parser.add_argument('-u', '--user', help = 'username for commands requiring authentication') @@ -272,10 +273,15 @@ def make_parser(): help = 'password command to evaluate for commands requiring authentication') parser.add_argument('-q', '--quiet', action='store_true', + default=None, help = 'quiet mode') + parser.add_argument('-d', '--debug', + type=int, + default=None, + help = 'debug level (from 0 to 3)') parser.add_argument('--columns', type = int, - help = 'maximum number of columns output should use') + help = 'maximum number of columns output should use (0 = unlimited)') parser.add_argument('--encoding', help = 'output encoding (default: utf-8).') parser.add_argument('--skip-auth', diff --git a/bugz/cli.py b/bugz/cli.py index 62ba540..7112387 100644 --- a/bugz/cli.py +++ b/bugz/cli.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - import commands import getpass from cookielib import CookieJar, LWPCookieJar @@ -12,6 +10,11 @@ import sys import tempfile import textwrap import xmlrpclib +import pdb + +from bugz.configfile import discover_configs +from bugz.log import * +from bugz.errhandling import BugzError try: import readline @@ -28,8 +31,8 @@ BUGZ: Any line beginning with 'BUGZ:' will be ignored. BUGZ: --------------------------------------------------- """ -DEFAULT_COOKIE_FILE = '.bugz_cookie' DEFAULT_NUM_COLS = 80 +DEFAULT_CONFIG_FILE = '/etc/pybugz/pybugz.conf' # # Auxiliary functions @@ -119,23 +122,63 @@ def block_edit(comment, comment_from = ''): else: return '' -# -# Bugz specific exceptions -# - -class BugzError(Exception): - pass - class PrettyBugz: + enc = "utf-8" + columns = 0 + quiet = None + skip_auth = None + + # TODO: + # * make this class more library-like (allow user to script on the python + # level using this PrettyBugz class) + # * get the "__init__" phase into main() and change parameters to accept + # only 'settings' structure def __init__(self, args): - self.quiet = args.quiet - self.columns = args.columns or terminal_width() - self.user = args.user - self.password = args.password - self.passwordcmd = args.passwordcmd - self.skip_auth = args.skip_auth - - cookie_file = os.path.join(os.environ['HOME'], DEFAULT_COOKIE_FILE) + + sys_config = DEFAULT_CONFIG_FILE + home_config = getattr(args, 'config_file') + setDebugLvl(getattr(args, 'debug')) + settings = discover_configs(sys_config, home_config) + + # use the default connection name + conn_name = settings['default'] + + # check for redefinition by --connection + opt_conn = getattr(args, 'connection') + if opt_conn != None: + conn_name = opt_conn + + if not conn_name in settings['connections']: + raise BugzError("can't find connection '{0}'".format(conn_name)) + + # get proper 'Connection' instance + connection = settings['connections'][conn_name] + + def fix_con(con, name,opt): + if opt != None: + setattr(con, name, opt) + con.option_change = True + + fix_con(connection, "base", args.base) + fix_con(connection, "quiet", args.quiet) + fix_con(connection, "columns", args.columns) + connection.columns = int(connection.columns) or terminal_width() + fix_con(connection, "user", args.user) + fix_con(connection, "password", args.password) + fix_con(connection, "password_cmd", args.passwordcmd) + fix_con(connection, "skip_auth", args.skip_auth) + fix_con(connection, "encoding", args.encoding) + + # now must the "connection" be complete + + # propagate layout settings to 'self' + self.enc = connection.encoding + self.skip_auth = connection.skip_auth + self.columns = connection.columns + + setQuiet(connection.quiet) + + cookie_file = os.path.expanduser(connection.cookie_file) self.cookiejar = LWPCookieJar(cookie_file) try: @@ -143,9 +186,7 @@ class PrettyBugz: except IOError: pass - if getattr(args, 'encoding'): - self.enc = args.encoding - else: + if not self.enc: try: self.enc = locale.getdefaultlocale()[1] except: @@ -153,19 +194,9 @@ class PrettyBugz: if not self.enc: self.enc = 'utf-8' - self.log("Using %s " % args.base) - self.bz = BugzillaProxy(args.base, cookiejar=self.cookiejar) - - def log(self, status_msg, newline = True): - if not self.quiet: - if newline: - print ' * %s' % status_msg - else: - print ' * %s' % status_msg, - - def warn(self, warn_msg): - if not self.quiet: - print ' ! Warning: %s' % warn_msg + self.bz = BugzillaProxy(connection.base, cookiejar=self.cookiejar) + connection.dump() + self.connection = connection def get_input(self, prompt): return raw_input(prompt) @@ -186,27 +217,29 @@ class PrettyBugz: """Authenticate a session. """ # prompt for username if we were not supplied with it - if not self.user: - self.log('No username given.') - self.user = self.get_input('Username: ') + if not self.connection.user: + log_info('No username given.') + self.connection.user = self.get_input('Username: ') # prompt for password if we were not supplied with it - if not self.password: - if not self.passwordcmd: - self.log('No password given.') - self.password = getpass.getpass() + if not self.connection.password: + if not self.connection.password_cmd: + log_info('No password given.') + self.connection.password = getpass.getpass() else: - process = subprocess.Popen(self.passwordcmd.split(), shell=False, - stdout=subprocess.PIPE) + cmd = self.connection.password_cmd.split() + stdout = stdout=subprocess.PIPE + process = subprocess.Popen(cmd, shell=False, stdout=stdout) self.password, _ = process.communicate() + self.connection.password, _ = process.communicate() # perform login params = {} - params['login'] = self.user - params['password'] = self.password + params['login'] = self.connection.user + params['password'] = self.connection.password if args is not None: params['remember'] = True - self.log('Logging in') + log_info('Logging in') try: self.bz.User.login(params) except xmlrpclib.Fault as fault: @@ -217,7 +250,7 @@ class PrettyBugz: os.chmod(self.cookiejar.filename, 0600) def logout(self, args): - self.log('logging out') + log_info('logging out') try: self.bz.User.logout() except xmlrpclib.Fault as fault: @@ -251,29 +284,39 @@ class PrettyBugz: else: log_msg = 'Searching for bugs ' - if search_opts: - self.log(log_msg + 'with the following options:') - for opt, val in search_opts: - self.log(' %-20s = %s' % (opt, val)) - else: - self.log(log_msg) - if not 'status' in params.keys(): - params['status'] = ['CONFIRMED', 'IN_PROGRESS', 'UNCONFIRMED'] - elif 'ALL' in params['status']: + if self.connection.query_statuses: + params['status'] = self.connection.query_statuses + else: + # this seems to be most portable among bugzillas as each + # bugzilla may have its own set of statuses. + params['status'] = ['ALL'] + + if 'ALL' in params['status']: del params['status'] + if len(params): + log_info(log_msg + 'with the following options:') + for opt, val in params.items(): + log_info(' %-20s = %s' % (opt, str(val))) + else: + log_info(log_msg) + result = self.bzcall(self.bz.Bug.search, params)['bugs'] if not len(result): - self.log('No bugs found.') + log_info('No bugs found.') else: self.listbugs(result, args.show_status) def get(self, args): """ Fetch bug details given the bug id """ - self.log('Getting bug %s ..' % args.bugid) - result = self.bzcall(self.bz.Bug.get, {'ids':[args.bugid]}) + log_info('Getting bug %s ..' % args.bugid) + try: + result = self.bzcall(self.bz.Bug.get, {'ids':[args.bugid]}) + except xmlrpclib.Fault as fault: + raise BugzError("Can't get bug #" + str(args.bugid) + ": " \ + + fault.faultString) for bug in result['bugs']: self.showbuginfo(bug, args.attachments, args.comments) @@ -293,7 +336,7 @@ class PrettyBugz: (args.description_from, e)) if not args.batch: - self.log('Press Ctrl+C at any time to abort.') + log_info('Press Ctrl+C at any time to abort.') # # Check all bug fields. @@ -306,14 +349,14 @@ class PrettyBugz: while not args.product or len(args.product) < 1: args.product = self.get_input('Enter product: ') else: - self.log('Enter product: %s' % args.product) + log_info('Enter product: %s' % args.product) # check for component if not args.component: while not args.component or len(args.component) < 1: args.component = self.get_input('Enter component: ') else: - self.log('Enter component: %s' % args.component) + log_info('Enter component: %s' % args.component) # check for version # FIXME: This default behaviour is not too nice. @@ -324,14 +367,14 @@ class PrettyBugz: else: args.version = 'unspecified' else: - self.log('Enter version: %s' % args.version) + log_info('Enter version: %s' % args.version) # check for title if not args.summary: while not args.summary or len(args.summary) < 1: args.summary = self.get_input('Enter title: ') else: - self.log('Enter title: %s' % args.summary) + log_info('Enter title: %s' % args.summary) # check for description if not args.description: @@ -339,7 +382,7 @@ class PrettyBugz: if len(line): args.description = line else: - self.log('Enter bug description: %s' % args.description) + log_info('Enter bug description: %s' % args.description) # check for operating system if not args.op_sys: @@ -348,7 +391,7 @@ class PrettyBugz: if len(line): args.op_sys = line else: - self.log('Enter operating system: %s' % args.op_sys) + log_info('Enter operating system: %s' % args.op_sys) # check for platform if not args.platform: @@ -357,7 +400,7 @@ class PrettyBugz: if len(line): args.platform = line else: - self.log('Enter hardware platform: %s' % args.platform) + log_info('Enter hardware platform: %s' % args.platform) # check for default priority if args.priority is None: @@ -366,7 +409,7 @@ class PrettyBugz: if len(line): args.priority = line else: - self.log('Enter priority (optional): %s' % args.priority) + log_info('Enter priority (optional): %s' % args.priority) # check for default severity if args.severity is None: @@ -375,7 +418,7 @@ class PrettyBugz: if len(line): args.severity = line else: - self.log('Enter severity (optional): %s' % args.severity) + log_info('Enter severity (optional): %s' % args.severity) # check for default alias if args.alias is None: @@ -384,7 +427,7 @@ class PrettyBugz: if len(line): args.alias = line else: - self.log('Enter alias (optional): %s' % args.alias) + log_info('Enter alias (optional): %s' % args.alias) # check for default assignee if args.assigned_to is None: @@ -393,7 +436,7 @@ class PrettyBugz: if len(line): args.assigned_to = line else: - self.log('Enter assignee (optional): %s' % args.assigned_to) + log_info('Enter assignee (optional): %s' % args.assigned_to) # check for CC list if args.cc is None: @@ -402,7 +445,7 @@ class PrettyBugz: if len(line): args.cc = line.split(', ') else: - self.log('Enter a CC list (optional): %s' % args.cc) + log_info('Enter a CC list (optional): %s' % args.cc) # check for URL if args.url is None: @@ -422,7 +465,7 @@ class PrettyBugz: if args.append_command is None: args.append_command = self.get_input('Append the output of the following command (leave blank for none): ') else: - self.log('Append command (optional): %s' % args.append_command) + log_info('Append command (optional): %s' % args.append_command) # raise an exception if mandatory fields are not specified. if args.product is None: @@ -470,7 +513,7 @@ class PrettyBugz: if len(confirm) < 1: confirm = args.default_confirm if confirm[0] not in ('y', 'Y'): - self.log('Submission aborted') + log_info('Submission aborted') return params={} @@ -498,7 +541,7 @@ class PrettyBugz: params['url'] = args.url result = self.bzcall(self.bz.Bug.create, params) - self.log('Bug %d submitted' % result['id']) + log_info('Bug %d submitted' % result['id']) def modify(self, args): """Modify an existing bug (eg. adding a comment or changing resolution.)""" @@ -604,16 +647,16 @@ class PrettyBugz: for bug in result['bugs']: changes = bug['changes'] if not len(changes): - self.log('Added comment to bug %s' % bug['id']) + log_info('Added comment to bug %s' % bug['id']) else: - self.log('Modified the following fields in bug %s' % bug['id']) + log_info('Modified the following fields in bug %s' % bug['id']) for key in changes.keys(): - self.log('%-12s: removed %s' %(key, changes[key]['removed'])) - self.log('%-12s: added %s' %(key, changes[key]['added'])) + log_info('%-12s: removed %s' %(key, changes[key]['removed'])) + log_info('%-12s: added %s' %(key, changes[key]['added'])) def attachment(self, args): """ Download or view an attachment given the id.""" - self.log('Getting attachment %s' % args.attachid) + log_info('Getting attachment %s' % args.attachid) params = {} params['attachment_ids'] = [args.attachid] @@ -621,7 +664,7 @@ class PrettyBugz: result = result['attachments'][args.attachid] action = {True:'Viewing', False:'Saving'} - self.log('%s attachment: "%s"' % + log_info('%s attachment: "%s"' % (action[args.view], result['file_name'])) safe_filename = os.path.basename(re.sub(r'\.\.', '', result['file_name'])) @@ -671,7 +714,7 @@ class PrettyBugz: params['comment'] = comment params['is_patch'] = is_patch result = self.bzcall(self.bz.Bug.add_attachment, params) - self.log("'%s' has been attached to bug %s" % (filename, bugid)) + log_info("'%s' has been attached to bug %s" % (filename, bugid)) def listbugs(self, buglist, show_status=False): for bug in buglist: @@ -690,7 +733,7 @@ class PrettyBugz: except UnicodeDecodeError: print line[:self.columns] - self.log("%i bug(s) found." % len(buglist)) + log_info("%i bug(s) found." % len(buglist)) def showbuginfo(self, bug, show_attachments, show_comments): FIELDS = ( diff --git a/bugz/configfile.py b/bugz/configfile.py index a900245..43a502c 100644 --- a/bugz/configfile.py +++ b/bugz/configfile.py @@ -1,70 +1,155 @@ import ConfigParser -import os +import os, glob import sys +import pdb -DEFAULT_CONFIG_FILE = '~/.bugzrc' - -def config_option(parser, get, section, option): - if parser.has_option(section, option): - try: - if get(section, option) != '': - return get(section, option) - else: - print " ! Error: "+option+" is not set" - sys.exit(1) - except ValueError, e: - print " ! Error: option "+option+" is not in the right format: "+str(e) - sys.exit(1) - -def fill_config_option(args, parser, get, section, option): - value = config_option(parser, get, section, option) - if value is not None: - setattr(args, option, value) - -def fill_config(args, parser, section): - fill_config_option(args, parser, parser.get, section, 'base') - fill_config_option(args, parser, parser.get, section, 'user') - fill_config_option(args, parser, parser.get, section, 'password') - fill_config_option(args, parser, parser.get, section, 'passwordcmd') - fill_config_option(args, parser, parser.getint, section, 'columns') - fill_config_option(args, parser, parser.get, section, 'encoding') - fill_config_option(args, parser, parser.getboolean, section, 'quiet') - -def get_config(args): - config_file = getattr(args, 'config_file') - if config_file is None: - config_file = DEFAULT_CONFIG_FILE - section = getattr(args, 'connection') - parser = ConfigParser.ConfigParser() - config_file_name = os.path.expanduser(config_file) - - # try to open config file - try: - file = open(config_file_name) - except IOError: - if getattr(args, 'config_file') is not None: - print " ! Error: Can't find user configuration file: "+config_file_name - sys.exit(1) - else: - return - - # try to parse config file +from bugz.errhandling import BugzError +from bugz.log import * + +class Connection: + name = "default" + base = 'https://bugs.gentoo.org/xmlrpc.cgi' + columns = 0 + user = None + password = None + password_cmd = None + dbglvl = 0 + quiet = None + skip_auth = None + encoding = "utf-8" + cookie_file = "~/.bugz_cookie" + option_change = False + query_statuses = [] + + def dump(self): + log_info("Using [{0}] ({1})".format(self.name, self.base)) + log_debug("User: '{0}'".format(self.user), 3) + # loglvl == 4, only for developers (&& only by hardcoding) + log_debug("Pass: '{0}'".format(self.password), 10) + log_debug("Columns: {0}".format(self.columns), 3) + +def handle_default(settings, newDef): + oldDef = str(settings['default']) + if oldDef != newDef: + log_debug("redefining default connection from '{0}' to '{1}'". \ + format(oldDef, newDef), 2) + settings['default'] = newDef + +def handle_settings(settings, context, stack, cp, sec_name): + log_debug("contains SETTINGS section named [{0}]".format(sec_name), 3) + + if cp.has_option(sec_name, 'homeconf'): + settings['homeconf'] = cp.get(sec_name, 'homeconf') + + if cp.has_option(sec_name, 'default'): + handle_default(settings, cp.get(sec_name, 'default')) + + # handle 'confdir' ~> explore and push target files into the stack + if cp.has_option(sec_name, 'confdir'): + confdir = cp.get(sec_name, 'confdir') + full_confdir = os.path.expanduser(confdir) + wildcard = os.path.join(full_confdir, '*.conf') + log_debug("adding wildcard " + wildcard, 3) + for cnffile in glob.glob(wildcard): + log_debug(" ++ " + cnffile, 3) + if cnffile in context['included']: + log_debug("skipping (already included)") + break + stack.append(cnffile) + +def handle_connection(settings, context, stack, parser, name): + log_debug("reading connection '{0}'".format(name), 2) + connection = None + + if name in settings['connections']: + log_debug("redefining connection '{0}'".format(name), 2) + connection = settings['connections'][name] + else: + connection = Connection() + connection.name = name + + def fill(conn, id): + if parser.has_option(name, id): + val = parser.get(name, id) + setattr(conn, id, val) + log_debug("has {0} - {1}".format(id, val), 3) + + fill(connection, "base") + fill(connection, "user") + fill(connection, "password") + fill(connection, "encoding") + fill(connection, "columns") + fill(connection, "quiet") + + if parser.has_option(name, 'query_statuses'): + line = parser.get(name, 'query_statuses') + lines = line.split() + connection.query_statuses = lines + + settings['connections'][name] = connection + +def parse_file(settings, context, stack): + file_name = stack.pop() + full_name = os.path.expanduser(file_name) + + context['included'][full_name] = None + + log_debug("parsing '" + file_name + "'", 1) + + cp = ConfigParser.ConfigParser() + parsed = None try: - parser.readfp(file) - sections = parser.sections() - except ConfigParser.ParsingError, e: - print " ! Error: Can't parse user configuration file: "+str(e) - sys.exit(1) - - # parse the default section first - if "default" in sections: - fill_config(args, parser, "default") - if section is None: - section = config_option(parser, parser.get, "default", "connection") - - # parse a specific section - if section in sections: - fill_config(args, parser, section) - elif section is not None: - print " ! Error: Can't find section ["+section+"] in configuration file" - sys.exit(1) + parsed = cp.read(full_name) + if parsed != [ full_name ]: + raise BugzError("problem with file '" + file_name + "'") + except ConfigParser.Error, err: + msg = err.message + raise BugzError("can't parse: '" + file_name + "'\n" + msg ) + + # successfully parsed file + + for sec in cp.sections(): + sectype = "connection" + + if cp.has_option(sec, 'type'): + sectype = cp.get(sec, 'type') + + if sectype == "settings": + handle_settings(settings, context, stack, cp, sec) + + if sectype == "connection": + handle_connection(settings, context, stack, cp, sec) + +def discover_configs(file, homeConf=None): + settings = { + # where to look for user's configuration + 'homeconf' : '~/.bugzrc', + # list of objects of Connection + 'connections' : {}, + # the default Connection name + 'default' : None, + } + context = { + 'where' : 'sys', + 'homeparsed' : False, + 'included' : {}, + } + stack = [ file ] + + # parse sys configs + while len(stack) > 0: + parse_file(settings, context, stack) + + if not homeConf: + # the command-line option must win + homeConf = settings['homeconf'] + + if not os.path.isfile(os.path.expanduser(homeConf)): + return settings + + # parse home configs + stack = [ homeConf ] + while len(stack) > 0: + parse_file(settings, context, stack) + + return settings diff --git a/bugz/errhandling.py b/bugz/errhandling.py new file mode 100644 index 0000000..d3fec06 --- /dev/null +++ b/bugz/errhandling.py @@ -0,0 +1,6 @@ +# +# Bugz specific exceptions +# + +class BugzError(Exception): + pass diff --git a/bugz/log.py b/bugz/log.py new file mode 100644 index 0000000..df4bb9a --- /dev/null +++ b/bugz/log.py @@ -0,0 +1,72 @@ +# TODO: use the python's 'logging' feature? + +dbglvl = 0 +quiet = False + +LogSettins = { + 'W' : { + 'symb' : '!', + 'word' : 'Warn', + }, + 'E' : { + 'symb' : '#', + 'word' : 'Error', + }, + 'D' : { + 'symb' : '~', + 'word' : 'Dbg', + }, + 'I' : { + 'symb' : '*', + 'word' : 'Info', + }, + '!' : { + 'symb' : '!', + 'word' : 'UNKNWN', + }, +} + +def setQuiet(newQuiet): + global quiet + quiet = newQuiet + +def setDebugLvl(newlvl): + global dbglvl + if not newlvl: + return + if newlvl > 3: + log_warn("bad debug level '{0}', using '3'".format(str(newlvl))) + dbglvl = 3 + else: + dbglvl = newlvl + +def formatOut(msg, id='!'): + lines = str(msg).split('\n') + start = True + symb=LogSettins[id]['symb'] + word=LogSettins[id]['word'] + ":" + + for line in lines: + print ' ' + symb + ' ' + line + +def log_error(string): + formatOut(string, 'E') + return + +def log_warn(string): + formatOut(string, 'W') + return + +def log_info(string): + global quiet + global dbglvl + # debug implies info + if not quiet or dbglvl: + formatOut(string, 'I') + return + +def log_debug(string, verboseness=1): + global dbglvl + if dbglvl >= verboseness: + formatOut(string, 'D') + return diff --git a/bugzrc.example b/bugzrc.example deleted file mode 100644 index f516bf0..0000000 --- a/bugzrc.example +++ /dev/null @@ -1,61 +0,0 @@ -# -# bugzrc.example - an example configuration file for pybugz -# -# This file consists of sections which define parameters for each -# bugzilla you plan to use. -# -# Each section begins with a name in square brackets. This is also the -# name that should be used with the --connection parameter to the bugz -# command. -# -# Each section of this file consists of lines in the form: -# key: value -# as listed below. -# -# [sectionname] -# -# The base url of the bugzilla you wish to use. -# This must point to the xmlrpc.cgi script on the bugzilla installation. -# -# base: http://my.project.com/bugzilla/xmlrpc.cgi -# -# It is also possible to encode a username and password into this URL -# for basic http authentication as follows: -# -# base: http://myhttpname:myhttppasswd@my.project.com/bugzilla/xmlrpc.cgi -# -# Next are your username and password for this bugzilla. If you do not -# provide these, you will be prompted for them. -# -# user: myname@my.project.com -# password: secret2 -# -# As an alternative to keeping your password in this file you can provide a -# password command. It is evaluated and pybugz expects this command to output -# the password to standard out. E.g.: -# -# passwordcmd: gpg2 --decrypt /myhome/.my-encrypted-password.gpg -# -# The number of columns your terminal can display. -# Most of the time you should not have to set this. -# -# columns: 80 -# -# Set the output encoding for pybugz. -# -# encoding: utf-8 -# -# Run in quiet mode. -# -# quiet: True -# -# The special section named 'default' may also be used. Other sections will -# override any values specified here. The optional special key 'connection' is -# used to name the default connection, to use when no --connection parameter is -# specified to the bugz command. -# -# [default] -# connection: sectionname -# -# All parameters listed above can be used in the default section if you -# only use one bugzilla installation. diff --git a/conf/conf.d/gentoo.conf b/conf/conf.d/gentoo.conf new file mode 100644 index 0000000..42fae46 --- /dev/null +++ b/conf/conf.d/gentoo.conf @@ -0,0 +1,3 @@ +[Gentoo] +base = https://bugs.gentoo.org/xmlrpc.cgi +query_statuses = CONFIRMED IN_PROGRESS UNCONFIRMED diff --git a/conf/conf.d/redhat.conf b/conf/conf.d/redhat.conf new file mode 100644 index 0000000..0f50fb7 --- /dev/null +++ b/conf/conf.d/redhat.conf @@ -0,0 +1,3 @@ +[RedHat] +base = https://bugzilla.redhat.com/xmlrpc.cgi +query_statuses = NEW ASSIGNED MODIFIED ON_DEV POST diff --git a/conf/pybugz.conf b/conf/pybugz.conf new file mode 100644 index 0000000..3a486b4 --- /dev/null +++ b/conf/pybugz.conf @@ -0,0 +1,126 @@ +# =========================================================================== +# The "root" configuration file of PyBugz bugzilla interface. +# =========================================================================== +# +# Overview +# ======== +# PyBugz is configured by hierarchy of *.conf files. All the configuration +# job starts from this file. User specific configuration is by default in +# file ~/.bugzrc. This is specially usefull to allow user to redefine some +# system configuration (an example could be adding user's credentials +# for specific connection — see the following text). +# +# Syntax +# ====== +# The syntax is similar to Windows INI files. For more info, see the +# documentation for python's ConfigParser library class — this class is used +# for parsing configuration files here. Quickly, each file consists of +# sections (section's name in brackets, e.g. [section]). Each section +# consists of set of configuration options separated by newlines. +# +# [sectionName] +# optionA = value A +# optionB = this is value of B # comments are possible +# +# Section types +# ============= +# Currently, there are implemented two types of sections in PyBugz. Those +# are 'connection' (default type of section) and 'settings'. +# Type 'settings' has purpose for setting up some global feature of PyBugz. +# The type 'connection', however, describes attributes of particular +# connection to some concrete instance of bugzilla. +# +# +------------------------+ +# | 1. "type = connection" | +# +------------------------+ +# +# Important property of this type is its section identifier (name of +# section). By passing this name as an argument of --connection option is +# PyBugz's user able to select which connection will be used. +# +# Accepted options / semantics +# ---------------------------- +# +# Note that you may specify each section of type 'connection' multiple +# times (using the same ID). All settings are combined among same named +# sections with one rule: the last one wins. This is important when you +# want to specify some defaults system wide and let particular user +# redefine (or correct) concrete connection — user's configuration is +# loaded _later_ than system's. +# +# * type +# May be set optionally to 'connection', but it is the default in each +# section. +# +# * base +# Sets up the xmlrpc entrance into bugzilla, for example: +# https://bugzilla.redhat.com/xmlrpc.cgi +# +# * user & password +# These two options let you specify your login information to bugzilla +# instance (you must be registered there of course). It is also +# possible to encode a user (usually user's email) and password into +# base: +# http://myhttpname:myhttppasswd@my.project.com/bugzilla/xmlrpc.cgi +# Note that if you don't specify your login information, you will be +# prompted for them. +# +# * passwordcmd +# As an alternative to keeping your password in this file you can +# provide a password command. It is evaluated and pybugz expects this +# command to output the password to standard out. E.g.: +# +# passwordcmd = gpg2 --decrypt /myhome/.my-encrypted-password.gpg +# +# * columns +# The number of columns your terminal can display (or you want to be +# displayed) during using of this connection. Expects integer number. +# +# * query_statuses +# List of bug-statuses to be displayed by default (when *not* redefined by +# --status option). Accepts list of properly spelled statuses separated +# by single space, e.g.: query_statuses = ASSIGNED CLOSED +# +# * encoding +# Set the output encoding for PyBugz. Default is utf-8. +# +# * quiet +# Run this connection in quiet mode when: quiet = True. +# +# * inherit (to be done in future) +# +# +----------------------+ +# | 2. "type = settings" | +# +----------------------+ +# +# Again, this lets you define PyBugz "global" settings (among all +# connections). The name of section is not important here. Same as +# 'connection' type, even this type of section you may define multiple +# times — options are combined then (and the latest wins). +# +# There are several accepted options (now): +# +# * type +# Here the type must be set to 'settings'. This is requirement for pybugz +# to interpret this section as you want. +# +# * default +# Lets you define the default connection (when the --connection option is +# not passed). +# +# * homeconf +# Let's you define where to look for user's configuration file. This is +# by default ~/.bugzrc file. Note that this option makes sense only for +# system-wide configuration file. +# +# * confdir +# This option lets you define the configuration directory. This directory +# is searched for *.conf files, and these files (if any) are parsed +# immediately after specifying configuration file. + +[settings] + +type = settings +homeconf = ~/.bugzrc +confdir = /etc/pybugz/conf.d/ +default = Gentoo diff --git a/man/bugz.1 b/man/bugz.1 index 628eae9..fbf2e6c 100644 --- a/man/bugz.1 +++ b/man/bugz.1 @@ -1,8 +1,8 @@ .\" Hey, Emacs! This is an -*- nroff -*- source file. -.\" Copyright (c) 2011 William Hubbs +.\" Copyright (c) 2011, 2012, 2013 William Hubbs .\" This is free software; see the GNU General Public Licence version 2 .\" or later for copying conditions. There is NO warranty. -.TH bugz 1 "17 Feb 2011" "0.9.0" +.TH bugz 1 "20 Jan 2013" "0.10.2" .nh .SH NAME bugz \(em command line interface to bugzilla @@ -20,10 +20,10 @@ bugz \(em command line interface to bugzilla .\" .B \-o value, \-\^\-long=value .\" Describe the option. .SH DESCRIPTION -Bugz is a cprogram which gives you access to the features of the +Bugz is a program which gives you access to the features of the bugzilla bug tracking system from the command line. .PP -This man page is a stub; the bugs program has extensive built in help. +This man page is a stub; the bugz program has extensive built in help. .B bugz -h will show the help for the global options and .B bugz [subcommand] -h @@ -32,8 +32,14 @@ will show the help for a specific subcommand. .PP The home page of this project is http://www.github.com/williamh/pybugz. Bugs should be reported to the bug tracker there. -.\" .SH SEE ALSO -.\" .PP +.SH SEE ALSO +.PP +For documentation how to configure PyBugz take a look into distributed +.B pybugz.conf +file. User specific configuration may be defined in +.B +~/.bugzrc +file. .SH AUTHOR .PP The original author is Alastair Tse . diff --git a/setup.py b/setup.py index e9a8a52..15f004c 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,9 @@ setup( platforms = ['any'], packages = ['bugz'], scripts = ['bin/bugz'], + data_files = [ + ('/etc/pybugz', ['conf/pybugz.conf']), + ('/etc/pybugz/conf.d', ['conf/conf.d/redhat.conf', 'conf/conf.d/gentoo.conf']), + ], cmdclass = {'build_py': build_py, 'build_scripts': build_scripts}, ) -- 1.7.11.7