diff --git a/README.rst b/README.rst index 6da813b..2769f1e 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ library for interacting with the Bugzilla_ bug tracking system, and plugins for version control systems that enable interaction with Bugzilla installations. -The only dependency is Python_ 2.7 to Pyhton_ 3.5 and bugzillatools works with +The only dependency is Python_ 2.7 to Python_ 3.5 and bugzillatools works with Bugzilla_ 4.0 or later where the XML-RPC feature is enabled. .. _Bugzilla: http://www.bugzilla.org/ @@ -52,6 +52,7 @@ The following subcommands are available: :desc: Show the description of the given bug(s). :dump: Print internal representation of bug data. :edit: Edit the given bugs. +:field: Show or update a specific field of given bugs. :fields: List valid values for bug fields. :help: Show help. :history: Show the history of the given bugs. diff --git a/bin/bugzilla b/bin/bugzilla old mode 100644 new mode 100755 diff --git a/bzlib/command.py b/bzlib/command.py index 6c42e8a..cdc0387 100644 --- a/bzlib/command.py +++ b/bzlib/command.py @@ -23,8 +23,9 @@ import itertools import re import textwrap +import pprint -from . import bug +from . import bug as bug_module from . import bugzilla from . import config from . import editor @@ -74,6 +75,28 @@ def decorator(cls): return decorator +def with_pretty(cls): + cls.args = cls.args + [ + lambda x: x.add_argument('--pretty', action='store_true', + help='Try to enhance display'), + ] + return cls + + +def with_field(cls): + cls.args = cls.args + [ + lambda x: x.add_argument('field', metavar='FIELD', help='Field name'), + ] + return cls + + +def with_id_only(cls): + cls.args = cls.args + [ + lambda x: x.add_argument('--number-only', action='store_true', + help='Display bug number only.'), + ] + return cls + def with_bugs(cls): cls.args = cls.args + [ lambda x: x.add_argument('bugs', metavar='BUG', type=int, nargs='+', @@ -121,15 +144,14 @@ def decorator(cls): return decorator -def with_server(cls): - def add_server_args(parser): - group = parser.add_argument_group('server arguments') - group.add_argument('--server', help='name of Bugzilla server to use') - group.add_argument('--url', help='base URL of Bugzilla server') - group.add_argument('--user', help='Bugzilla username') - group.add_argument('--password', help='Bugzilla password') - cls.args = cls.args + [add_server_args] - return cls +def with_group(name, arguments, description=None): + def decorator(cls): + def add_argument_group(parser): + group = parser.add_argument_group(name, description) + arguments(cls, group) + cls.args = cls.args + [add_argument_group] + return cls + return decorator class Command(object): @@ -237,7 +259,14 @@ def __call__(self): self._parser.parse_args([self._args.subcommand, '--help']) -@with_server +def _make_server_arguments_group(cls, group): + group.add_argument('--server', help='name of Bugzilla server to use') + group.add_argument('--url', help='base URL of Bugzilla server') + group.add_argument('--user', help='Bugzilla username') + group.add_argument('--password', help='Bugzilla password') + + +@with_group('server arguments', _make_server_arguments_group) class BugzillaCommand(Command): def __init__(self, *args, **kwargs): super(BugzillaCommand, self).__init__(*args, **kwargs) @@ -393,8 +422,8 @@ def cmtfmt(bug): print('\n'.join(map(cmtfmt, args.bugs))) -@with_set('given bugs', 'depdendencies', metavar='BUG', type=int) -@with_add_remove('given bugs', 'depdendencies', metavar='BUG', type=int) +@with_set('given bugs', 'dependencies', metavar='BUG', type=int) +@with_add_remove('given bugs', 'dependencies', metavar='BUG', type=int) @with_bugs @with_optional_message class Depend(BugzillaCommand): @@ -441,12 +470,19 @@ def _descfmt(bug): print('\n'.join(_descfmt(bug) for bug in self._args.bugs)) +@with_pretty @with_bugs class Dump(BugzillaCommand): """Print internal representation of bug data.""" def __call__(self): - bugs = (self.bz.bug(x) for x in self._args.bugs) - print('\n'.join(str((x.data, x.comments)) for x in bugs)) + args = self._args + bugs = (self.bz.bug(x) for x in args.bugs) + if args.pretty: + for x in bugs: + pprint.pprint(x.data) + pprint.pprint(x.comments) + else: + print('\n'.join(str((x.data, x.comments)) for x in bugs)) @with_bugs @@ -468,11 +504,33 @@ def __call__(self): } bug.update(**kwargs) +@with_set('given bugs', 'a given field', metavar='VALUE') +@with_bugs +@with_field +class Field(BugzillaCommand): + """Show or update a given field of given bugs.""" + def __call__(self): + args = self._args + field = args.field + for bug in map(self.bz.bug, args.bugs): + if field not in bug.data: + raise UserWarning("Invalid field:", field) + data = bug.data[field] + if args.set is not None: + kwargs = { + field: args.set if isinstance(data, list) else args.set[0], + } + bug.update(**kwargs) + else: + if isinstance(data, list): + data = ', '.join(data) + print('Bug {bugno}:\n' + ' {field}: {data}'.format(bugno=bug.bugno, field=field, data=data)) + class Fields(BugzillaCommand): """List valid values for bug fields.""" def __call__(self): - args = self._args fields = filter(lambda x: 'values' in x, self.bz.get_fields()) for field in fields: keyfn = lambda x: x.get('visibility_values') @@ -507,7 +565,6 @@ def _format_history(history): class History(BugzillaCommand): """Show the history of the given bugs.""" def __call__(self): - fields = ('WHO', 'WHEN', 'WHAT', 'REMOVED', 'ADDED') for bug in map(self.bz.bug, self._args.bugs): history = [] for h in bug.history: @@ -556,7 +613,7 @@ class New(BugzillaCommand): """File a new bug.""" def __call__(self): # create new Bug - b = bug.Bug(self.bz) + b = bug_module.Bug(self.bz) # get mandatory fields fields = self.bz.get_fields() @@ -772,33 +829,32 @@ def __call__(self): ) -def _make_set_argument(arg): - template = 'Only match bugs {{}}of the given {}({})'.format( - arg, 's' if arg[-1] != 's' else 'es') - return [ - lambda x: x.add_argument('--' + arg, nargs='+', + +def _make_set_arguments_group(cls, group): + group.add_argument('--summary', nargs='+', + help='Match summary against any of the given substrings.') + + for arg in cls.set_arguments: + template = 'Only match bugs {{}}of the given {}({})'.format( + arg, 's' if arg[-1] != 's' else 'es') + group.add_argument('--' + arg, nargs='+', metavar=arg.upper(), help=template.format('')), - lambda x: x.add_argument('--not-' + arg, nargs='+', + group.add_argument('--not-' + arg, nargs='+', metavar=arg.upper(), help=template.format('NOT ')), - ] +@with_id_only +@with_group('search criteria', _make_set_arguments_group) class Search(BugzillaCommand): """Search for bugs matching given criteria. If both '--foo' and '--not-foo' are given for any argument 'foo', the former takes precendence. """ - args = BugzillaCommand.args + [ - lambda x: x.add_argument('--summary', nargs='+', - help='Match summary against any of the given substrings.'), - ] simple_arguments = ['summary'] - set_arguments = 'product', 'component', 'status', 'resolution', 'version' - for x in set_arguments: - args.extend(_make_set_argument(x)) + set_arguments = 'product', 'component', 'status', 'resolution', 'version', 'assigned_to' def __call__(self): kwargs = { @@ -811,13 +867,17 @@ def __call__(self): if getattr(self._args, arg) } - bugs = list(bug.Bug.search(self.bz, **kwargs)) + bugs = list(bug_module.Bug.search(self.bz, **kwargs)) lens = [len(str(b.bugno)) for b in bugs] - for _bug in bugs: + if self._args.number_only: + print(' '.join(map(lambda x: str(x.bugno), bugs))) + return + + for bug in bugs: print('Bug {:{}} {}'.format( - str(_bug.bugno) + ':', max(lens) - min(lens) + 2, - _bug.data['summary'] + str(bug.bugno) + ':', max(lens) - min(lens) + 2, + bug.data['summary'] )) n = len(bugs) print('=> {} bug{} matched criteria'.format(n, 's' if n else '')) @@ -874,5 +934,5 @@ def __call__(self): lambda x: type(x) == type # is a class \ and issubclass(x, Command) # is a Command \ and x not in [Command, BugzillaCommand], # not abstract - locals().items() + locals().values() )