diff options
author | W. Trevor King <wking@drexel.edu> | 2009-12-31 15:54:12 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2009-12-31 15:54:12 -0500 |
commit | b0b5341c4045dd27cfbb3e2585cb2614ed9ad903 (patch) | |
tree | 37c7c2d011617ccd7a6f28a24ea77bb1b3cddfe7 /libbe/ui | |
parent | a06030436d3940dddfba37b344f90651366d67e1 (diff) | |
parent | 2d1562d951e763fed71fe60e77cc9921be9abdc9 (diff) | |
download | bugseverywhere-b0b5341c4045dd27cfbb3e2585cb2614ed9ad903.tar.gz |
Merged be.restructure, major internal reorganization.
Added a bunch of classes to make the commands, user interfaces, and
storage backends more abstract and distinct. This should make it much
easier to extend and maintain BE.
Features:
* Directory restructured:
becommands/ -> libbe/commands
submods sorted by functionality.
* Lots of new classes:
Option, Argument, Command
InputOutput, StorageCallbacks, UserInterface
Storage
* Consolidated ID handling in libbe.util.id
* Transitioned VCS backends for Python-based VCSs from subprocess
calss to internal python calls.
Plus the user-visible changes:
* New bugdir/bug/comment ID format replaces old bug:comment format.
* Deprecated support for `be diff` on Arch and Darcs <= 2.3.1. A new
backend abstraction (Storage) makes the former implementation
ungainly.
* Improved command completion.
* Removed commands close, open, email_bugs,
* Flipped some arguments
`be assign BUG-ID [ASSIGNEE]` -> `be status ASSIGNED BUG-ID ...`
`be severity BUG-ID SEVERITY` -> `be severity SEVERITY BUG-ID ...`
`be status BUG-ID STATUS` -> `be status STATUS BUG-ID ...`
In the merge:
* Added 'commit' to list of pagerless commands.
* Updated doc/README.dev
See
#bea86499-824e-4e77-b085-2d581fa9ccab/1100c966-9671-4bc6-8b68-6d408a910da1#
for a discussion of why the changes were made and some of the
difficulties en-route.
Diffstat (limited to 'libbe/ui')
-rw-r--r-- | libbe/ui/__init__.py | 1 | ||||
-rw-r--r-- | libbe/ui/command_line.py | 317 | ||||
-rw-r--r-- | libbe/ui/util/__init__.py | 1 | ||||
-rw-r--r-- | libbe/ui/util/editor.py | 116 | ||||
-rw-r--r-- | libbe/ui/util/pager.py | 65 | ||||
-rw-r--r-- | libbe/ui/util/user.py | 89 |
6 files changed, 589 insertions, 0 deletions
diff --git a/libbe/ui/__init__.py b/libbe/ui/__init__.py new file mode 100644 index 0000000..b98f164 --- /dev/null +++ b/libbe/ui/__init__.py @@ -0,0 +1 @@ +# Copyright diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py new file mode 100644 index 0000000..7f74782 --- /dev/null +++ b/libbe/ui/command_line.py @@ -0,0 +1,317 @@ +# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc. +# Chris Ball <cjb@laptop.org> +# Gianluca Montecchi <gian@grys.it> +# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com> +# W. Trevor King <wking@drexel.edu> +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +A command line interface to Bugs Everywhere. +""" + +import optparse +import os +import sys + +import libbe +import libbe.bugdir +import libbe.command +import libbe.command.util +import libbe.version +import libbe.ui.util.pager + +if libbe.TESTING == True: + import doctest + +class CallbackExit (Exception): + pass + +class CmdOptionParser(optparse.OptionParser): + def __init__(self, command): + self.command = command + optparse.OptionParser.__init__(self) + self.remove_option('-h') + self.disable_interspersed_args() + self._option_by_name = {} + for option in self.command.options: + self._add_option(option) + self.set_usage(command.usage()) + + + def _add_option(self, option): + option.validate() + self._option_by_name[option.name] = option + long_opt = '--%s' % option.name + if option.short_name != None: + short_opt = '-%s' % option.short_name + assert '_' not in option.name, \ + 'Non-reconstructable option name %s' % option.name + kwargs = {'dest':option.name.replace('-', '_'), + 'help':option.help} + if option.arg == None: # a callback option + kwargs['action'] = 'callback' + kwargs['callback'] = self.callback + elif option.arg.type == 'bool': + kwargs['action'] = 'store_true' + kwargs['metavar'] = None + kwargs['default'] = False + else: + kwargs['type'] = option.arg.type + kwargs['action'] = 'store' + kwargs['metavar'] = option.arg.metavar + kwargs['default'] = option.arg.default + if option.short_name != None: + opt = optparse.Option(short_opt, long_opt, **kwargs) + else: + opt = optparse.Option(long_opt, **kwargs) + opt._option = option + self.add_option(opt) + + def parse_args(self, args=None, values=None): + args = self._get_args(args) + options,parsed_args = optparse.OptionParser.parse_args( + self, args=args, values=values) + options = options.__dict__ + for name,value in options.items(): + if '_' in name: # reconstruct original option name + options[name.replace('_', '-')] = options.pop(name) + for name,value in options.items(): + if value == '--complete': + argument = None + option = self._option_by_name[name] + if option.arg != None: + argument = option.arg + fragment = None + indices = [i for i,arg in enumerate(args) + if arg == '--complete'] + for i in indices: + assert i > 0 # this --complete is an option value + if args[i-1] in ['--%s' % o.name + for o in self.command.options]: + name = args[i-1][2:] + if name == option.name: + break + elif option.short_name != None \ + and args[i-1].startswith('-') \ + and args[i-1].endswith(option.short_name): + break + if i+1 < len(args): + fragment = args[i+1] + self.complete(argument, fragment) + for i,arg in enumerate(parsed_args): + if arg == '--complete': + if i > 0 and self.command.name == 'be': + break # let this pass through for the command parser to handle + elif i < len(self.command.args): + argument = self.command.args[i] + elif len(self.command.args) == 0: + break # command doesn't take arguments + else: + argument = self.command.args[-1] + if argument.repeatable == False: + raise libbe.command.UserError('Too many arguments') + fragment = None + if i < len(parsed_args) - 1: + fragment = parsed_args[i+1] + self.complete(argument, fragment) + if len(parsed_args) > len(self.command.args) \ + and self.command.args[-1].repeatable == False: + raise libbe.command.UserError('Too many arguments') + for arg in self.command.args[len(parsed_args):]: + if arg.optional == False: + raise libbe.command.UserError( + 'Missing required argument %s' % arg.metavar) + return (options, parsed_args) + + def callback(self, option, opt, value, parser): + command_option = option._option + if command_option.name == 'complete': + argument = None + fragment = None + if len(parser.rargs) > 0: + fragment = parser.rargs[0] + self.complete(argument, fragment) + else: + print >> self.command.stdout, command_option.callback( + self.command, command_option, value) + raise CallbackExit + + def complete(self, argument=None, fragment=None): + comps = self.command.complete(argument, fragment) + if fragment != None: + comps = [c for c in comps if c.startswith(fragment)] + if len(comps) > 0: + print >> self.command.stdout, '\n'.join(comps) + raise CallbackExit + +class BE (libbe.command.Command): + """Class for parsing the command line arguments for `be`. + This class does not contain a useful _run() method. Call this + module's main() function instead. + + >>> ui = libbe.command.UserInterface() + >>> ui.io.stdout = sys.stdout + >>> be = BE(ui=ui) + >>> ui.io.setup_command(be) + >>> p = CmdOptionParser(be) + >>> p.exit_after_callback = False + >>> try: + ... options,args = p.parse_args(['--help']) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + ... except CallbackExit: + ... pass + usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]] + <BLANKLINE> + Options: + -h, --help Print a help message. + <BLANKLINE> + --complete Print a list of possible completions. + <BLANKLINE> + --version Print version string. + ... + >>> try: + ... options,args = p.parse_args(['--complete']) # doctest: +ELLIPSIS + ... except CallbackExit: + ... print ' got callback' + --help + --complete + --version + ... + subscribe + tag + target + got callback + """ + name = 'be' + + def __init__(self, *args, **kwargs): + libbe.command.Command.__init__(self, *args, **kwargs) + self.options.extend([ + libbe.command.Option(name='version', + help='Print version string.', + callback=self.version), + libbe.command.Option(name='full-version', + help='Print full version information.', + callback=self.full_version), + libbe.command.Option(name='repo', short_name='r', + help='Select BE repository (see `be help repo`) rather ' + 'than the current directory.', + arg=libbe.command.Argument( + name='repo', metavar='REPO', default='.', + completion_callback=libbe.command.util.complete_path)), + libbe.command.Option(name='paginate', + help='Pipe all output into less (or if set, $PAGER).'), + libbe.command.Option(name='no-pager', + help='Do not pipe git output into a pager.'), + ]) + self.args.extend([ + libbe.command.Argument( + name='command', optional=False, + completion_callback=libbe.command.util.complete_command), + libbe.command.Argument( + name='args', optional=True, repeatable=True) + ]) + + def usage(self): + return 'usage: be [options] [COMMAND [command-options] [COMMAND-ARGS ...]]' + + def _long_help(self): + cmdlist = [] + for name in libbe.command.commands(): + Class = libbe.command.get_command_class(command_name=name) + assert hasattr(Class, '__doc__') and Class.__doc__ != None, \ + 'Command class %s missing docstring' % Class + cmdlist.append((name, Class.__doc__.splitlines()[0])) + cmdlist.sort() + longest_cmd_len = max([len(name) for name,desc in cmdlist]) + ret = ['Bugs Everywhere - Distributed bug tracking', + '', 'Supported commands'] + for name, desc in cmdlist: + numExtraSpaces = longest_cmd_len-len(name) + ret.append('be %s%*s %s' % (name, numExtraSpaces, '', desc)) + ret.extend(['', 'Run', ' be help [command]', 'for more information.']) + return '\n'.join(ret) + + def version(self, *args): + return libbe.version.version(verbose=False) + + def full_version(self, *args): + return libbe.version.version(verbose=True) + +def dispatch(ui, command, args): + parser = CmdOptionParser(command) + try: + options,args = parser.parse_args(args) + ret = ui.run(command, options, args) + except CallbackExit: + return 0 + except libbe.command.UserError, e: + print >> ui.io.stdout, 'ERROR:\n', e + return 1 + except libbe.storage.ConnectionError, e: + print >> ui.io.stdout, 'Connection Error:\n', e + return 1 + except (libbe.util.id.MultipleIDMatches, libbe.util.id.NoIDMatches, + libbe.util.id.InvalidIDStructure), e: + print >> ui.io.stdout, 'Invalid id:\n', e + return 1 + finally: + command.cleanup() + return ret + +def main(): + io = libbe.command.StdInputOutput() + ui = libbe.command.UserInterface(io) + ui.restrict_file_access = False + ui.storage_callbacks = None + be = BE(ui=ui) + ui.setup_command(be) + + parser = CmdOptionParser(be) + try: + options,args = parser.parse_args() + except CallbackExit: + return 0 + except libbe.command.UserError, e: + print >> ui.io.stdout, 'ERROR:\n', e + return 1 + + command_name = args.pop(0) + try: + Class = libbe.command.get_command_class(command_name=command_name) + except libbe.command.UnknownCommand, e: + print >> ui.io.stdout, e + return 1 + + ui.storage_callbacks = libbe.command.StorageCallbacks(options['repo']) + command = Class(ui=ui) + ui.setup_command(command) + + if command.name in ['comment', 'commit']: + paginate = 'never' + else: + paginate = 'auto' + if options['paginate'] == True: + paginate = 'always' + if options['no-pager'] == True: + paginate = 'never' + libbe.ui.util.pager.run_pager(paginate) + + ret = dispatch(ui, command, args) + ui.cleanup() + return ret + +if __name__ == '__main__': + sys.exit(main()) diff --git a/libbe/ui/util/__init__.py b/libbe/ui/util/__init__.py new file mode 100644 index 0000000..b98f164 --- /dev/null +++ b/libbe/ui/util/__init__.py @@ -0,0 +1 @@ +# Copyright diff --git a/libbe/ui/util/editor.py b/libbe/ui/util/editor.py new file mode 100644 index 0000000..1a10fa4 --- /dev/null +++ b/libbe/ui/util/editor.py @@ -0,0 +1,116 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi <gian@grys.it> +# W. Trevor King <wking@drexel.edu> +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Define editor_string(), a function that invokes an editor to accept +user-produced text as a string. +""" + +import codecs +import locale +import os +import sys +import tempfile + +import libbe +import libbe.util.encoding + +if libbe.TESTING == True: + import doctest + + +comment_marker = u"== Anything below this line will be ignored\n" + +class CantFindEditor(Exception): + def __init__(self): + Exception.__init__(self, "Can't find editor to get string from") + +def editor_string(comment=None, encoding=None): + """Invokes the editor, and returns the user-produced text as a string + + >>> if "EDITOR" in os.environ: + ... del os.environ["EDITOR"] + >>> if "VISUAL" in os.environ: + ... del os.environ["VISUAL"] + >>> editor_string() + Traceback (most recent call last): + CantFindEditor: Can't find editor to get string from + >>> os.environ["EDITOR"] = "echo bar > " + >>> editor_string() + u'bar\\n' + >>> os.environ["VISUAL"] = "echo baz > " + >>> editor_string() + u'baz\\n' + >>> os.environ["VISUAL"] = "echo 'baz\\n== Anything below this line will be ignored\\nHi' > " + >>> editor_string() + u'baz\\n' + >>> del os.environ["EDITOR"] + >>> del os.environ["VISUAL"] + """ + if encoding == None: + encoding = libbe.util.encoding.get_filesystem_encoding() + for name in ('VISUAL', 'EDITOR'): + try: + editor = os.environ[name] + break + except KeyError: + pass + else: + raise CantFindEditor() + fhandle, fname = tempfile.mkstemp() + try: + if comment is not None: + cstring = u'\n'+comment_string(comment) + os.write(fhandle, cstring.encode(encoding)) + os.close(fhandle) + oldmtime = os.path.getmtime(fname) + os.system("%s %s" % (editor, fname)) + output = libbe.util.encoding.get_file_contents( + fname, encoding=encoding, decode=True) + output = trimmed_string(output) + if output.rstrip('\n') == "": + output = None + finally: + os.unlink(fname) + return output + + +def comment_string(comment): + """ + >>> comment_string('hello') == comment_marker+"hello" + True + """ + return comment_marker + comment + + +def trimmed_string(instring): + """ + >>> trimmed_string("hello\\n"+comment_marker) + u'hello\\n' + >>> trimmed_string("hi!\\n" + comment_string('Booga')) + u'hi!\\n' + """ + out = [] + for line in instring.splitlines(True): + if line.startswith(comment_marker): + break + out.append(line) + return ''.join(out) + +if libbe.TESTING == True: + suite = doctest.DocTestSuite() diff --git a/libbe/ui/util/pager.py b/libbe/ui/util/pager.py new file mode 100644 index 0000000..1ddc3fa --- /dev/null +++ b/libbe/ui/util/pager.py @@ -0,0 +1,65 @@ +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Automatic pager for terminal output (a la Git). +""" + +import sys, os, select + +# see http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby +def run_pager(paginate='auto'): + """ + paginate should be one of 'never', 'auto', or 'always'. + + usage: just call this function and continue using sys.stdout like + you normally would. + """ + if paginate == 'never' \ + or sys.platform == 'win32' \ + or not hasattr(sys.stdout, 'isatty') \ + or sys.stdout.isatty() == False: + return + + if paginate == 'auto': + if 'LESS' not in os.environ: + os.environ['LESS'] = '' # += doesn't work on undefined var + # don't page if the input is short enough + os.environ['LESS'] += ' -FRX' + if 'PAGER' in os.environ: + pager = os.environ['PAGER'] + else: + pager = 'less' + + read_fd, write_fd = os.pipe() + if os.fork() == 0: + # child process + os.close(read_fd) + os.close(0) + os.dup2(write_fd, 1) + os.close(write_fd) + if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() == True: + os.dup2(1, 2) + return + + # parent process, become pager + os.close(write_fd) + os.dup2(read_fd, 0) + os.close(read_fd) + + # Wait until we have input before we start the pager + select.select([0], [], []) + os.execlp(pager, pager) diff --git a/libbe/ui/util/user.py b/libbe/ui/util/user.py new file mode 100644 index 0000000..d6af89b --- /dev/null +++ b/libbe/ui/util/user.py @@ -0,0 +1,89 @@ +# Copyright + +""" +Tools for getting, setting, creating, and parsing the user's id. For +example, + 'John Doe <jdoe@example.com>' +Note that the Arch VCS backend *enforces* ids with this format. +""" + +import os +import re +from socket import gethostname + +import libbe +import libbe.storage.util.config + +def get_fallback_username(): + name = None + for env in ["LOGNAME", "USERNAME"]: + if os.environ.has_key(env): + name = os.environ[env] + break + assert name != None + return name + +def get_fallback_email(): + hostname = gethostname() + name = get_fallback_username() + return "%s@%s" % (name, hostname) + +def create_user_id(name, email=None): + """ + >>> create_user_id("John Doe", "jdoe@example.com") + 'John Doe <jdoe@example.com>' + >>> create_user_id("John Doe") + 'John Doe' + """ + assert len(name) > 0 + if email == None or len(email) == 0: + return name + else: + return "%s <%s>" % (name, email) + +def parse_user_id(value): + """ + >>> parse_user_id("John Doe <jdoe@example.com>") + ('John Doe', 'jdoe@example.com') + >>> parse_user_id("John Doe") + ('John Doe', None) + >>> try: + ... parse_user_id("John Doe <jdoe@example.com><what?>") + ... except AssertionError: + ... print "Invalid match" + Invalid match + """ + emailexp = re.compile("(.*) <([^>]*)>(.*)") + match = emailexp.search(value) + if match == None: + email = None + name = value + else: + assert len(match.groups()) == 3 + assert match.groups()[2] == "", match.groups() + email = match.groups()[1] + name = match.groups()[0] + assert name != None + assert len(name) > 0 + return (name, email) + +def get_user_id(storage=None): + """ + Sometimes the storage will also keep track of the user id (e.g. most VCSs). + """ + user = libbe.storage.util.config.get_val('user') + if user != None: + return user + if storage != None and hasattr(storage, 'get_user_id'): + user = storage.get_user_id() + if user != None: + return user + name = get_fallback_username() + email = get_fallback_email() + user = create_user_id(name, email) + return user + +def set_user_id(user_id): + """ + """ + user = libbe.storage.util.config.set_val('user', user_id) |