intermediate commit for subcommand dispatcher
authorAlexander Solovyov <piranha@piranha.org.ua>
Sat, 20 Jun 2009 17:00:54 +0300
changeset 12 f1590d2363ff
parent 11 560f5682ce00
child 13 c1857739143c
intermediate commit for subcommand dispatcher
fancycmd.py
fancyopts.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fancycmd.py	Sat Jun 20 17:00:54 2009 +0300
@@ -0,0 +1,223 @@
+import sys, traceback, getopt
+
+from fancyopts import parse
+
+def help_(ui, header, cmdtable, globalopts, name=None):
+    def helplist():
+        hlp = {}
+        for cmd, info in cmdtable.items():
+            # TODO: Handle situation when there is no
+            # commands starting with '^'!
+            if name == 'shortlist' and not cmd.startswith('^'):
+                continue # short help contains only marked commands
+            cmd = cmd.lstrip('^')
+            doc = info[0].__doc__ or '(no help text available)'
+            hlp[cmd] = doc.splitlines()[0].rstrip()
+
+        ui.status(header)
+        hlp = sorted(hlp)
+        maxlen = max(map(len, hlp))
+        for cmd, doc in hlp.items():
+            if ui.verbose:
+                ui.write(' %s:\n     %s\n' % (cmd.replace('|', ', '), doc))
+            else:
+                ui.write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0], doc))
+
+    if not cmdtable:
+        ui.warn('No commands specified!\n')
+        return
+
+    if not name or name == 'shortlist':
+        helplist()
+
+
+def dispatch(args, cmdtable, globalopts=None):
+    '''Dispatch command arguments based on subcommands.
+
+     - ``args``: sys.argv[1:]
+     - ``cmdtable``: dict of commands in next format::
+
+     {'name': (function, options, usage)}
+
+     - ``globalopts``: list of options which are applied to all
+       commands, if not supplied will contain ``--help`` option
+
+    where:
+
+     - ``name`` is the name used on command-line. Can containt
+       aliases (separate them with '|') or pointer to the fact
+       that this command should be displayed in short help (start
+       name with '^')
+     - ``function`` is the actual callable
+     - ``options`` is options list in fancyopts format
+     - ``usage`` is the short string of usage
+    '''
+
+    ui = UI()
+    if not globalopts:
+        globalopts = [('h', 'help', False, 'display help')]
+
+    try:
+        return _dispatch(ui, args, cmdtable, globalopts + UI.options)
+    except Abort, e:
+        ui.warn('abort: %s' % e)
+    except UnknownCommand, e:
+        ui.warn("unknown command: '%s'" % e)
+    except AmbiguousCommand, e:
+        ui.warn("command '%s' is ambiguous:\n    %s\n" %
+                (e.args[0], ' '.join(e.args[1])))
+    except ParseError, e:
+        if e.args[0]:
+            ui.warn('%s: %s' % (e.args[0], e.args[1]))
+            # display help here?
+        else:
+            ui.warn("%s\n" % e.args[1])
+            help_(ui, 'Preved', cmdtable, globalopts, 'shortlist')
+    except KeyboardInterrupt:
+        ui.warn('interrupted!\n')
+    except:
+        ui.warn('unknown exception encountered')
+        raise
+
+    return -1
+
+def _dispatch(ui, args, cmdtable, globalopts):
+    cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
+                                                       globalopts)
+
+    ui.verbose = globaloptions['verbose']
+    ui.quiet = globaloptions['quiet']
+
+    if globaloptions['help']:
+        pass # help
+    elif not cmd:
+        import ipdb; ipdb.set_trace()
+        return help_(ui, '', cmdtable, globalopts, 'shortlist')
+
+    try:
+        return func(ui, *args, **options)
+    except TypeError:
+        if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
+            raise ParseError(cmd, "invalid arguments")
+        raise
+
+def cmdparse(args, cmdtable, globalopts):
+    # command is the first non-option
+    cmd = None
+    for arg in args:
+        if not arg.startswith('-'):
+            cmd = arg
+            break
+
+    if cmd:
+        args.pop(args.index(cmd))
+
+        aliases, info = findcmd(cmd, cmdtable)
+        cmd = aliases[0]
+        possibleargs = list(info[1])
+    else:
+        possibleargs = []
+
+    possibleargs.extend(globalopts)
+
+    try:
+        options, args = parse(args, possibleargs)
+    except getopt.GetoptError, e:
+        raise ParseError(cmd, e)
+
+    globaloptions = {}
+    for o in globalopts:
+        name = o[1]
+        globaloptions[name] = options.pop(name)
+
+    return (cmd, cmd and info[0] or None, args, options, globaloptions)
+
+def findpossible(cmd, table):
+    """
+    Return cmd -> (aliases, command table entry)
+    for each matching command.
+    """
+    choice = {}
+    for e in table.keys():
+        aliases = e.lstrip("^").split("|")
+        found = None
+        if cmd in aliases:
+            found = cmd
+        else:
+            for a in aliases:
+                if a.startswith(cmd):
+                    found = a
+                    break
+        if found is not None:
+            choice[found] = (aliases, table[e])
+
+    return choice
+
+def findcmd(cmd, table):
+    """Return (aliases, command table entry) for command string."""
+    choice = findpossible(cmd, table)
+
+    if cmd in choice:
+        return choice[cmd]
+
+    if len(choice) > 1:
+        clist = choice.keys()
+        clist.sort()
+        raise AmbiguousCommand(cmd, clist)
+
+    if choice:
+        return choice.values()[0]
+
+    raise UnknownCommand(cmd)
+
+class UI(object):
+    '''User interface helper.
+
+    Intended to ease handling of quiet/verbose output and more.
+
+    You have three methods to handle program messages output:
+
+      - ``UI.status`` is printed by default, but hidden with quiet option
+      - ``UI.note`` is printed only if output is verbose
+      - ``UI.write`` is printed in any case
+
+    Additionally there is ``UI.warn`` method, which prints to stderr.
+    '''
+
+    options = [('v', 'verbose', False, 'enable additional output'),
+               ('q', 'quiet', False, 'suppress output')]
+
+    def __init__(self, verbose=False, quiet=False):
+        self.verbose = verbose
+        self.quiet = quiet
+
+    def write(self, *messages):
+        for m in messages:
+            sys.stdout.write(m)
+
+    def warn(self, *messages):
+        for m in messages:
+            sys.stderr.write(m)
+
+    status = lambda self, *m: not self.quiet and self.write(*m)
+    note = lambda self, *m: self.verbose and self.write(*m)
+
+# Command exceptions
+class CommandException(Exception):
+    'Base class for command exceptions'
+
+class Abort(CommandException):
+    'Raised if an error in command occured'
+
+class AmbiguousCommand(CommandException):
+    'Raised if command is ambiguous'
+
+class UnknownCommand(CommandException):
+    'Raised if command is unknown'
+
+class ParseError(CommandException):
+    'Raised on error in command line parsing'
+
+class SignatureError(CommandException):
+    'Raised if function signature does not correspond to arguments'
+
--- a/fancyopts.py	Tue Jun 16 07:13:56 2009 +0300
+++ b/fancyopts.py	Sat Jun 20 17:00:54 2009 +0300
@@ -45,12 +45,12 @@
 
 def fancyopts(cmd, usage, options, args):
     if not args:
-        help_(cmd, usage, options)
+        cmd_help(cmd, usage, options)
     else:
         opts, args = parse(args, options)
         cmd(*args, **opts)
 
-def help_(cmd, usage, options):
+def cmd_help(cmd, usage, options):
     '''show help for given command
 
     >>> def test(*args, **opts):
@@ -66,7 +66,7 @@
     ...          'daemonize process'),
     ...         ('', 'pid-file', '',
     ...          'name of file to write process ID to')]
-    >>> help_(test, 'test [-l HOST] [NAME]', opts)
+    >>> cmd_help(test, 'test [-l HOST] [NAME]', opts)
     test [-l HOST] [NAME]
     <BLANKLINE>
     that's a test command
@@ -81,13 +81,13 @@
         --pid-file   name of file to write process ID to
     <BLANKLINE>
     '''
+    print '%s\n' % usage
     doc = cmd.__doc__
     if not doc:
         doc = '(no help text available)'
-    print '%s\n\n%s\n' % (usage, doc.strip())
+    print '%s\n' % doc.strip()
     print ''.join(help_options(options))
 
-
 def help_options(options):
     yield 'options:\n\n'
     output = []