fancyopts merged in fancycmd to become single python module
authorAlexander Solovyov <piranha@piranha.org.ua>
Sat, 27 Jun 2009 22:17:05 +0300
changeset 21 b05cd4f91f53
parent 20 a1423afc1160
child 22 8e56f2a8b90a
fancyopts merged in fancycmd to become single python module
fancycmd.py
fancyopts.py
test.py
test_opts.py
--- a/fancycmd.py	Sat Jun 27 20:58:12 2009 +0300
+++ b/fancycmd.py	Sat Jun 27 22:17:05 2009 +0300
@@ -1,7 +1,80 @@
-import sys, traceback, getopt
+# (c) Alexander Solovyov, 2009, under terms of the new BSD License
+'''Fancy command line arguments parser
+'''
+
+import sys, traceback, getopt, types, textwrap
 from itertools import imap
 
-from fancyopts import parse, cmd_help
+__all__ = ['fancyopts', 'dispatch']
+
+# --------
+# Public interface
+# --------
+
+def fancyopts(cmd, usage, options, args):
+    if not args:
+        cmd_help(cmd, usage, options)
+    else:
+        opts, args = parse(args, options)
+        cmd(*args, **opts)
+
+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'),
+            # is not used yet
+            ('', 'traceback', False, 'display full traceback on error')]
+
+    cmdtable['help'] = (help_(cmdtable, globalopts), [], '[TOPIC]')
+
+    try:
+        return _dispatch(ui, args, cmdtable, globalopts + UI.options)
+    except Abort, e:
+        ui.warn('abort: %s\n' % e)
+    except UnknownCommand, e:
+        ui.warn("unknown command: '%s'\n" % e)
+    except AmbiguousCommand, e:
+        ui.warn("command '%s' is ambiguous:\n    %s\n" %
+                (e.args[0], ' '.join(e.args[1])))
+    except ParseError, e:
+        ui.warn('%s: %s\n' % (e.args[0], e.args[1]))
+        cmdtable['help'][0](ui, e.args[0])
+    except KeyboardInterrupt:
+        ui.warn('interrupted!\n')
+    except SystemExit:
+        raise
+    except:
+        ui.warn('unknown exception encountered')
+        raise
+
+    return -1
+
+
+# --------
+# Help
+# --------
 
 def help_(cmdtable, globalopts):
     def inner(ui, name=None):
@@ -44,57 +117,132 @@
         return cmd_help(cmd, aliases[0]  + ' ' + usage, options)
     return inner
 
-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
+def cmd_help(cmd, usage, options):
+    '''show help for given command
 
-    where:
+    >>> def test(*args, **opts):
+    ...     """that's a test command
+    ...
+    ...        you can do nothing with this command"""
+    ...     pass
+    >>> opts = [('l', 'listen', 'localhost',
+    ...          'ip to listen on'),
+    ...         ('p', 'port', 8000,
+    ...          'port to listen on'),
+    ...         ('d', 'daemonize', False,
+    ...          'daemonize process'),
+    ...         ('', 'pid-file', '',
+    ...          'name of file to write process ID to')]
+    >>> cmd_help(test, 'test [-l HOST] [NAME]', opts)
+    test [-l HOST] [NAME]
+    <BLANKLINE>
+    that's a test command
+    <BLANKLINE>
+           you can do nothing with this command
+    <BLANKLINE>
+    options:
+    <BLANKLINE>
+     -l --listen     ip to listen on (default: localhost)
+     -p --port       port to listen on (default: 8000)
+     -d --daemonize  daemonize process
+        --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' % doc.strip()
+    if options:
+        print ''.join(help_options(options))
 
-     - ``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
-    '''
+def help_options(options):
+    yield 'options:\n\n'
+    output = []
+    for short, name, default, desc in options:
+        default = default and ' (default: %s)' % default or ''
+        output.append(('%2s%s' % (short and '-%s' % short,
+                                  name and ' --%s' % name),
+                       '%s%s' % (desc, default)))
+
+    opts_len = max([len(first) for first, second in output if second] or [0])
+    for first, second in output:
+        if second:
+            # wrap description at 70 chars
+            second = textwrap.wrap(second, width=(70 - opts_len - 3))
+            pad = '\n' + ' ' * (opts_len + 3)
+            yield ' %-*s  %s\n' % (opts_len, first, pad.join(second))
+        else:
+            yield '%s\n' % first
+
+
+# --------
+# Options parsing
+# --------
 
-    ui = UI()
-    if not globalopts:
-        globalopts = [
-            ('h', 'help', False, 'display help'),
-            ('', 'traceback', False, 'display full traceback on error')]
+def parse(args, options):
+    '''
+    >>> opts = [('l', 'listen', 'localhost',
+    ...          'ip to listen on'),
+    ...         ('p', 'port', 8000,
+    ...          'port to listen on'),
+    ...         ('d', 'daemonize', False,
+    ...          'daemonize process'),
+    ...         ('', 'pid-file', '',
+    ...          'name of file to write process ID to')]
+    >>> print parse(['-l', '0.0.0.0', '--pi', 'test', 'all'], opts)
+    ({'pid_file': 'test', 'daemonize': False, 'port': 8000, 'listen': '0.0.0.0'}, ['all'])
 
-    cmdtable['help'] = (help_(cmdtable, globalopts), [], '[TOPIC]')
+    '''
+    argmap, defmap, state = {}, {}, {}
+    shortlist, namelist = '', []
+
+    for short, name, default, comment in options:
+        # change name to match Python styling
+        pyname = name.replace('-', '_')
+        argmap['-' + short] = argmap['--' + name] = pyname
+        defmap[pyname] = default
+
+        # copy defaults to state
+        if isinstance(default, list):
+            state[pyname] = default[:]
+        elif hasattr(default, '__call__'):
+            state[pyname] = None
+        else:
+            state[pyname] = default
 
-    try:
-        return _dispatch(ui, args, cmdtable, globalopts + UI.options)
-    except Abort, e:
-        ui.warn('abort: %s\n' % e)
-    except UnknownCommand, e:
-        ui.warn("unknown command: '%s'\n" % e)
-    except AmbiguousCommand, e:
-        ui.warn("command '%s' is ambiguous:\n    %s\n" %
-                (e.args[0], ' '.join(e.args[1])))
-    except ParseError, e:
-        ui.warn('%s: %s\n' % (e.args[0], e.args[1]))
-        cmdtable['help'][0](ui, e.args[0])
-    except KeyboardInterrupt:
-        ui.warn('interrupted!\n')
-    except SystemExit:
-        raise
-    except:
-        ui.warn('unknown exception encountered')
-        raise
+        # getopt wants indication that it takes a parameter
+        if not (default is None or default is True or default is False):
+            if short: short += ':'
+            if name: name += '='
+        if short:
+            shortlist += short
+        if name:
+            namelist.append(name)
+
+    opts, args = getopt.gnu_getopt(args, shortlist, namelist)
 
-    return -1
+    # transfer result to state
+    for opt, val in opts:
+        name = argmap[opt]
+        t = type(defmap[name])
+        if t is types.FunctionType:
+            state[name] = defmap[name](val)
+        elif t is types.IntType:
+            state[name] = int(val)
+        elif t is types.StringType:
+            state[name] = val
+        elif t is types.ListType:
+            state[name].append(val)
+        elif t in (types.NoneType, types.BooleanType):
+            state[name] = not defmap[name]
+
+    return state, args
+
+
+# --------
+# Subcommand system
+# --------
 
 def _dispatch(ui, args, cmdtable, globalopts):
     cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
@@ -185,6 +333,11 @@
 
     raise UnknownCommand(cmd)
 
+
+# --------
+# UI and exceptions
+# --------
+
 class UI(object):
     '''User interface helper.
 
@@ -237,3 +390,6 @@
 class SignatureError(CommandException):
     'Raised if function signature does not correspond to arguments'
 
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
--- a/fancyopts.py	Sat Jun 27 20:58:12 2009 +0300
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-# (c) Alexander Solovyov, 2009, under terms of the new BSD License
-'''Fancy option parser
-
-Usage::
-
-  >>> def serve(dirname, **opts):
-  ...     """this is a do-nothing command
-  ...
-  ...        you can do nothing with this command"""
-  ...     print opts.get('listen'), opts.get('pid_file')
-  >>> opts = [('l', 'listen', 'localhost',
-  ...          'ip to listen on'),
-  ...         ('p', 'port', 8000,
-  ...          'port to listen on'),
-  ...         ('d', 'daemonize', False,
-  ...          'daemonize process'),
-  ...         ('', 'pid-file', '',
-  ...          'name of file to write process ID to')]
-  >>> from fancyopts import fancyopts
-  >>> fancyopts(serve, 'serve [-l HOST] DIR', opts, '--pid-f test dir'.split())
-  localhost test
-
-You have supplied directory name here and path to file with process id.
-Order of options is preserved.
-
-Each option definition is a tuple consisting of 4 elements:
-
- - short name
- - long name
- - default value
- - help string
-
-Default value determines option type, which is selected from this choices:
-
- - function: return value of function called with a specified value is passed
- - integer: value is convert to integer
- - string: value is passed as is
- - list: value is appended to this list
- - boolean/None: `not default` is passed
-'''
-
-import getopt, types, textwrap
-
-__all__ = ['fancyopts']
-
-def fancyopts(cmd, usage, options, args):
-    if not args:
-        cmd_help(cmd, usage, options)
-    else:
-        opts, args = parse(args, options)
-        cmd(*args, **opts)
-
-def cmd_help(cmd, usage, options):
-    '''show help for given command
-
-    >>> def test(*args, **opts):
-    ...     """that's a test command
-    ...
-    ...        you can do nothing with this command"""
-    ...     pass
-    >>> opts = [('l', 'listen', 'localhost',
-    ...          'ip to listen on'),
-    ...         ('p', 'port', 8000,
-    ...          'port to listen on'),
-    ...         ('d', 'daemonize', False,
-    ...          'daemonize process'),
-    ...         ('', 'pid-file', '',
-    ...          'name of file to write process ID to')]
-    >>> cmd_help(test, 'test [-l HOST] [NAME]', opts)
-    test [-l HOST] [NAME]
-    <BLANKLINE>
-    that's a test command
-    <BLANKLINE>
-           you can do nothing with this command
-    <BLANKLINE>
-    options:
-    <BLANKLINE>
-     -l --listen     ip to listen on (default: localhost)
-     -p --port       port to listen on (default: 8000)
-     -d --daemonize  daemonize process
-        --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' % doc.strip()
-    if options:
-        print ''.join(help_options(options))
-
-def help_options(options):
-    yield 'options:\n\n'
-    output = []
-    for short, name, default, desc in options:
-        default = default and ' (default: %s)' % default or ''
-        output.append(('%2s%s' % (short and '-%s' % short,
-                                  name and ' --%s' % name),
-                       '%s%s' % (desc, default)))
-
-    opts_len = max([len(first) for first, second in output if second] or [0])
-    for first, second in output:
-        if second:
-            # wrap description at 70 chars
-            second = textwrap.wrap(second, width=(70 - opts_len - 3))
-            pad = '\n' + ' ' * (opts_len + 3)
-            yield ' %-*s  %s\n' % (opts_len, first, pad.join(second))
-        else:
-            yield '%s\n' % first
-
-
-def parse(args, options):
-    '''
-    >>> opts = [('l', 'listen', 'localhost',
-    ...          'ip to listen on'),
-    ...         ('p', 'port', 8000,
-    ...          'port to listen on'),
-    ...         ('d', 'daemonize', False,
-    ...          'daemonize process'),
-    ...         ('', 'pid-file', '',
-    ...          'name of file to write process ID to')]
-    >>> print parse(['-l', '0.0.0.0', '--pi', 'test', 'all'], opts)
-    ({'pid_file': 'test', 'daemonize': False, 'port': 8000, 'listen': '0.0.0.0'}, ['all'])
-
-    '''
-    argmap, defmap, state = {}, {}, {}
-    shortlist, namelist = '', []
-
-    for short, name, default, comment in options:
-        # change name to match Python styling
-        pyname = name.replace('-', '_')
-        argmap['-' + short] = argmap['--' + name] = pyname
-        defmap[pyname] = default
-
-        # copy defaults to state
-        if isinstance(default, list):
-            state[pyname] = default[:]
-        elif hasattr(default, '__call__'):
-            state[pyname] = None
-        else:
-            state[pyname] = default
-
-        # getopt wants indication that it takes a parameter
-        if not (default is None or default is True or default is False):
-            if short: short += ':'
-            if name: name += '='
-        if short:
-            shortlist += short
-        if name:
-            namelist.append(name)
-
-    opts, args = getopt.gnu_getopt(args, shortlist, namelist)
-
-    # transfer result to state
-    for opt, val in opts:
-        name = argmap[opt]
-        t = type(defmap[name])
-        if t is types.FunctionType:
-            state[name] = defmap[name](val)
-        elif t is types.IntType:
-            state[name] = int(val)
-        elif t is types.StringType:
-            state[name] = val
-        elif t is types.ListType:
-            state[name].append(val)
-        elif t in (types.NoneType, types.BooleanType):
-            state[name] = not defmap[name]
-
-    return state, args
-
-if __name__ == '__main__':
-    import doctest
-    doctest.testmod()
--- a/test.py	Sat Jun 27 20:58:12 2009 +0300
+++ b/test.py	Sat Jun 27 22:17:05 2009 +0300
@@ -37,7 +37,7 @@
         (complex_,
          [('p', 'pass', False, 'don\'t run the command'),
           ('', 'exit', 0, 'exit with supplied code (default: 0)')],
-         '')}
+         '[-p] [--exit value] ...')}
 
 if __name__ == '__main__':
     dispatch(sys.argv[1:], cmdtable)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test_opts.py	Sat Jun 27 22:17:05 2009 +0300
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import sys
+
+from fancycmd import fancyopts
+
+opts = [('l', 'listen', 'localhost', 'ip to listen on'),
+        ('p', 'port', 8000, 'port to listen on'),
+        ('d', 'daemonize', False, 'daemonize process'),
+        ('', 'pid-file', '', 'name of file to write process ID to')]
+
+def main(dirname, **opts):
+    '''This is some command
+
+    It looks very similar to some serve command
+    '''
+    print opts.get('pid_file')
+
+if __name__ == '__main__':
+    fancyopts(main, '%s [-l HOST] DIR' % sys.argv[0], opts, sys.argv[1:])