Skip to content
Open
3 changes: 2 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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.
Expand Down
Empty file modified bin/bugzilla
100644 → 100755
Empty file.
134 changes: 97 additions & 37 deletions bzlib/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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='+',
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 ''))
Expand Down Expand Up @@ -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()
)