create new commander modules for commands, begin tests
3 files changed, 325 insertions(+), 229 deletions(-)

M test/test_cli.py => test/test_cli_commands.py
M vanity/cli.py
A => vanity/commander.py
M test/test_cli.py => test/test_cli_commands.py +43 -228
@@ 1,248 1,63 @@ 
 
 import unittest
 
-from vanity import cli
-
-
-SAMPLE1 = '--output foo.txt bar.txt'.split()
-SAMPLE2 = 'bar.txt -o foo.txt -e --verbose'.split()
-
-TABLE_A = [
-    ('output', 'o', '', 'Output file'),
-    ('extra', 'e', None, 'Add extra markers'),
-    ('verbose', '', None, 'Be verbose'),
-    ]
-
-# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
+from vanity import commander
 
-OPTIONS1 = [
-    ('template', 't', '', 'Set template file'),
-    ('output', 'o', '', 'Output file'),
-    ('format', '', '', 'Input/Output format'),
-    ('check', 'c', None, 'Validate input before processing'),
-    ('verbose', '', None, 'Be verbose'),
+GOPT_A = [
+    ('verbose', 'v', None, 'be verbose'), 
+    ('quiet', 'q', None, 'be quiet'),
     ]
 
-CLI1 = 'a.txt b.txt'
-CLI2 = '-o out.txt a.txt'
-CLI3 = '--verbose -t foo.x -o out.txt b.txt'
-CLI4 = '--bad --verbose c.txt'
-CLI5 = '-c a.txt -o moose.txt'
-
-OPTIONS2 = [
-    ('run-tests', '', None, 'Run tests'),
-    ('skip-checks', '', None, 'Skip detailed checks'),
-    ('set-modules', '', '.', 'Location for modules'),
-    ('set-destination', 'd', '.', 'Destination directory'),
-    ]
-
-CLI6 = ''
-CLI7 = '--run-tests --skip-checks --set-modules /tmp --set-destination=/tmp'
-CLI8 = '-d /home/foo -q'
-CLI9 = '--set-destination --run-tests'
-
 
 def cmd1():
-    pass
-def cmd2():
-    pass
-def cmd3():
-    pass
+    return 1
 
-OPTIONS3 = [
-    ('debug', 'D', None, 'Enable debugging'),
-    ('help',  '?', None, 'Show help'),
-    ]
-COMMANDS1 = [
-    (cmd1, 'draw', OPTIONS2, 'Draw on slate'),
-    (cmd2, 'wipe', [], 'Wipe slate'),
-    (cmd3, 'window', [], 'Open a new window'),
-    (cmd3, 'fit', [], 'rezise objects to fit on slate'),
-    (cmd3, 'fitness', [], 'check if all objects fit'),
-    ]
+def cmd2():
+    return 2
 
-CLI10 = 'draw fribble.s'
-CLI11 = '--debug draw fribble.s'
-CLI12 = 'wipe --debug fribble.s'
-CLI13 = 'dr --help -d /home/blarg --skip-checks fribble.s'
-CLI14 = '--help --skip-checks dr -d . fribble.s'
-CLI15 = '--help'
-CLI16 = "fit womp.s"
-CLI17 = "fi womp.s"
-    
+def cmd3():
+    return 3
 
 
-# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-
-"""
-class XXXTestCli(unittest.TestCase):
-    def test_cli_1(self):
-        opts, args = cli.parse(OPTIONS1, CLI1.split())
-        self.assertTrue(args == ['a.txt', 'b.txt']) 
-        self.assertTrue(opts['template'] == '')
-        self.assertTrue(opts['output'] == '')
-        self.assertTrue(opts['format'] == '')
-        self.assertFalse(opts['check'])
-        self.assertFalse(opts['verbose'])
-
-    def test_cli_2(self):
-        opts, args = cli.parse(OPTIONS1, CLI2.split())
-        self.assertTrue(args == ['a.txt']) 
-        self.assertTrue(opts['template'] == '')
-        self.assertTrue(opts['output'] == 'out.txt')
-        self.assertTrue(opts['format'] == '')
-        self.assertFalse(opts['check'])
-        self.assertFalse(opts['verbose'])
-
-    def test_cli_3(self):
-        opts, args = cli.parse(OPTIONS1, CLI3.split())
-        self.assertTrue(args == ['b.txt']) 
-        self.assertTrue(opts['template'] == 'foo.x')
-        self.assertTrue(opts['output'] == 'out.txt')
-        self.assertTrue(opts['format'] == '')
-        self.assertFalse(opts['check'])
-        self.assertTrue(opts['verbose'])
-
-    def test_cli_4(self):
-        self.assertRaises(
-            cli.InvalidOption,
-            cli.parse,
-            OPTIONS1, CLI4.split()
-            )
+CMDTABLE_A = [
+    ('foo', cmd1, '', [], 'foo fixes everything'),
+    ('bar', cmd2, '', [], 'bar breaks it'),
+    ('baz', cmd3, '', [
+        ('shrimp', '', None, 'jumbo shrimp'),
+        ], 'something helpful'),
+    ]
 
-    def test_cli_5(self):
-        opts, args = cli.parse(OPTIONS1, CLI5.split())
-        self.assertTrue(args == ['a.txt']) 
-        self.assertTrue(opts['template'] == '')
-        self.assertTrue(opts['output'] == 'moose.txt')
-        self.assertTrue(opts['format'] == '')
-        self.assertTrue(opts['check'])
-        self.assertFalse(opts['verbose'])
-
-    def test_cli_6(self):
-        opts, args = cli.parse(OPTIONS2, CLI6.split())
-        self.assertTrue(args == [])
-        self.assertFalse(opts.get('run-tests'))
-        self.assertFalse(opts.get('skip-checks'))
-        self.assertTrue(opts.get('set-modules') == '.')
-        self.assertTrue(opts.get('set-destination') == '.')
+class TestCliCommands(unittest.TestCase):
 
-    def test_cli_7(self):
-        opts, args = cli.parse(OPTIONS2, CLI7.split())
-        self.assertTrue(args == [])
-        self.assertTrue(opts.get('run-tests'))
-        self.assertTrue(opts.get('skip-checks'))
-        self.assertTrue(opts.get('set-modules') == '/tmp')
-        self.assertTrue(opts.get('set-destination') == '/tmp')
-
-    def test_cli_8(self):
-        self.assertRaises(
-            cli.InvalidOption,
-            cli.parse,
-            OPTIONS2, CLI8.split()
-            )
-
-    def test_cli_9(self):
-        opts, args = cli.parse(OPTIONS2, CLI9.split())
-        self.assertTrue(args == [])
-        self.assertFalse(opts.get('run-tests'))
-        self.assertFalse(opts.get('skip-checks'))
-        self.assertTrue(opts.get('set-modules') == '.')
-        # a screwed up command line
-        self.assertTrue(opts.get('set-destination') == '--run-tests')
+    def test_parse_cli_cmd(self):
+        arguments = 'foo test.txt'.split()
+        cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+        self.assertEqual(cmd.target, cmd1)
+        self.assertEqual(args, ['test.txt'])
 
-    def test_cli_10(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI10.split())
-        self.assertTrue(fn == cmd1)
-        self.assertTrue('debug' in opts)
-        self.assertFalse(opts['debug'])
-        self.assertTrue('help' in opts)
-        self.assertFalse(opts['help'])
-        self.assertTrue('run-tests' in opts)
-
-    def test_cli_11(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI11.split())
-        self.assertTrue(fn == cmd1)
-        self.assertTrue('debug' in opts)
-        self.assertTrue(opts['debug'])
-        self.assertTrue('help' in opts)
-        self.assertFalse(opts['help'])
-        self.assertTrue('run-tests' in opts)
-
-    def test_cli_12(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI12.split())
-        self.assertTrue(fn == cmd2)
-        self.assertTrue('debug' in opts)
-        self.assertTrue(opts['debug'])
-        self.assertTrue('help' in opts)
-        self.assertFalse(opts['help'])
-        self.assertFalse('run-tests' in opts)
-
-    def test_cli_13(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI13.split())
-        self.assertTrue(fn == cmd1)
-        self.assertTrue('debug' in opts)
-        self.assertFalse(opts['debug'])
-        self.assertTrue('help' in opts)
-        self.assertTrue(opts['help'])
-        self.assertTrue('run-tests' in opts)
-
-    def test_cli_14(self):
-        self.assertRaises(
-            cli.InvalidOption,
-            cli.parsecommand,
-            OPTIONS3, COMMANDS1, CLI14.split()
-            )
+    def test_parse_cli_cmd2(self):
+        arguments = 'foo -v test.txt'.split()
+        cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+        self.assertEqual(cmd.target, cmd1)
+        self.assertEqual(args, ['test.txt'])
+        self.assertEqual(opts, dict(verbose=True, quiet=None))
 
-    def test_cli_15(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI15.split())
-        self.assertTrue(fn == None)
-        self.assertTrue(args == [])
-        self.assertTrue('debug' in opts)
-        self.assertTrue('help' in opts)
-        self.assertFalse(opts['debug'])
-        self.assertTrue(opts['help'])
-
-    def test_cli_16(self):
-        fn, opts, args = cli.parsecommand(OPTIONS3, COMMANDS1, CLI16.split())
-        self.assertTrue(fn == cmd3)
-
-    def test_cli_17(self):
-        self.assertRaises(
-            cli.AmbiguousCommand,
-            cli.parsecommand,
-            OPTIONS3, COMMANDS1, CLI17.split()
-            )
-
+    def test_parse_cli_cmd3(self):
+        arguments = '-v foo test.txt --quiet'.split()
+        cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+        self.assertEqual(cmd.target, cmd1)
+        self.assertEqual(args, ['test.txt'])
+        self.assertEqual(opts, dict(verbose=True, quiet=True))
 
-    def test_parse_sample_1(self):
-        opts, args = cli.parse(TABLE_A, SAMPLE1)
-        self.assertTrue('output' in opts)
-        self.assertTrue('extra' in opts)
-        self.assertTrue('verbose' in opts)
-        self.assertTrue(opts['output'] == 'foo.txt')
-        self.assertFalse(opts['extra'])
-        self.assertFalse(opts['verbose'])
-        self.assertTrue(args == ['bar.txt'])
-
-    def test_parse_sample_2(self):
-        opts, args = cli.parse(TABLE_A, SAMPLE2)
-        self.assertTrue('output' in opts)
-        self.assertTrue('extra' in opts)
-        self.assertTrue('verbose' in opts)
-        self.assertTrue(opts['output'] == 'foo.txt')
-        self.assertTrue(opts['extra'])
-        self.assertTrue(opts['verbose'])
-        self.assertTrue(args == ['bar.txt'])
-
-    def test_usage(self):
-        ug = list(cli.usage(TABLE_A))
-        #print '\n%s\n' % '\n'.join(ug)
-        self.assertTrue(len(ug) == 3)
-
-"""
-
-
+    def test_parse_cli_cmd4(self):
+        arguments = '-v baz test.txt --shrimp'.split()
+        cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+        self.assertEqual(cmd.target, cmd3)
+        self.assertEqual(args, ['test.txt'])
+        self.assertEqual(opts, dict(verbose=True, quiet=None, shrimp=True))
+        arguments = '-v baz test.txt'.split()
+        cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments)
+        self.assertEqual(opts, dict(verbose=True, quiet=None, shrimp=None))
 
 
 if __name__ == '__main__':

          
M vanity/cli.py +8 -1
@@ 85,11 85,18 @@ def parse(table, arguments, strict=None)
     * ``arguments`` - the argument list to be parsed
     * ``strict`` - if true, use strict posix style parsing
     """
+    table = OptionTable(table)
+    return _parse(table, arguments, strict)
+
+
+def _parse(table, arguments, strict):
+    """Lower level implementation of cli.parse.
+    Table must be a constructed CommandTable instance.
+    """
     if strict:
         getopt = _getopt.getopt
     else:
         getopt = _getopt.gnu_getopt
-    table = OptionTable(table)
     try:
         raw, args = getopt(arguments, table.shortoptspec(), table.longoptspec())
     except _getopt.GetoptError, ee:

          
A => vanity/commander.py +274 -0
@@ 0,0 1,274 @@ 
+#!/usr/bin/env python
+#
+# Copyright 2009  John Mulligan <phlogistonjohn@asynchrono.us>
+#
+# This software may be used and distributed according to the terms of the
+# MIT license, incorporated herein by reference. A copy of this license
+# should accompany the source code in a file named COPYING.txt.
+#
+"""parse command lines with subcommands
+
+
+XXX
+ret = commander.launch(globalopts, cmdtable, args)
+
+cmd, opts, args = commander.parse(globalopts, cmdtable, args)
+
+XXX
+"""
+
+from vanity import cli
+
+
+class InvalidCommand(cli.CliError):
+    """The CLI was given an invalid command"""
+    pass
+
+
+class AmbiguousCommand(cli.CliError):
+    """The given command was ambigouous"""
+    def __init__(self, matches):
+        msg = 'possible matches: %s' % ' '.join(matches)
+        CliError.__init__(self, msg)
+        self.matches = matches
+
+
+def launch(globalopts, cmdtable, arguments):
+    """Launch a subcommand based on the given arguments, return the
+    result of the launched function.
+
+    * ``globalopts`` - an options table common to all sub commands
+    * ``cmdtable`` -  a commands table
+    * ``arguments`` - the command line arguments
+    """
+    pass
+
+
+def parse(globalopts, cmdtable, arguments):
+    """Parse a cli and return the corresponding command, parsed options
+    and arguments.
+
+    * ``globalopts`` - an options table common to all sub commands
+    * ``cmdtable`` -  a commands table
+    * ``arguments`` - the command line arguments
+    """
+    gopts = EarlyOptionTable(globalopts)
+    cmds = CommandTable(cmdtable)
+    opts, args = cli._parse(gopts, arguments, True)
+    # TODO : handle missing command
+    cname, args = args[0], args[1:]
+    cmd = cmds.find(cname)
+    copts, cargs = cli._parse(cmd.opts + gopts, args, cmd.strict)
+    copts.update(opts)
+    return cmd, copts, cargs
+    
+
+class Command(object):
+    """A thin class representing a command.
+
+    Attributes: name, target, aliases, help, strict
+    """
+
+    def __init__(self, name, target, aliases=None, opts=None, 
+                 help=None, strict=None):
+        self.name = name
+        self.target = target
+        self.aliases = aliases
+        self.help = help
+        self.opts = cli.OptionTable(opts)
+        self.strict = strict
+
+    def __call__(self, opts, args):
+        return self.target(*args, **opts)
+
+    def aliaslist(self):
+        if self.aliases:
+            return self.aliases.split('|')
+        else:
+            return []
+
+    @classmethod
+    def convert(cls, obj):
+        if isinstance(obj, cls):
+            return obj
+        if hasattr(obj, 'keys'):
+            return cls(**obj)
+        return cls(*obj)
+
+
+class EarlyOptionTable(cli.OptionTable):
+    """Special case options table for before-command opts"""
+
+    def assemble(self, opts):
+        """an assemble function that will not return all keys"""
+        assembled = cli.OptionTable.assemble(self, opts)
+        return dict((name, assembled[name]) for name in self._names(opts))
+
+    def _names(self, opts):
+        for key, _ in opts:
+            try:
+                yield self.getlong(key).name
+            except KeyError:
+                yield self.getshort(key).name
+
+
+class CommandTable(object):
+    """A table of launchable sub-commands.
+    """
+
+    def __init__(self, table):
+        self._table = {}
+        if hasattr(table, 'keys'):
+            # if a user is giving us a dict, the entries **must** be dicts
+            for name, entry in table:
+                self._table[name] = Command(name, **entry)
+        else:
+            for entry in table:
+                cmd = Command.convert(entry)
+                self._table[cmd.name] = cmd
+        return
+
+    def __iter__(self):
+        return self._table.itervalues()
+
+    def find(self, name):
+        """Find the command entry that best matches the given name.
+        Returns a command, if no matches are possible an InvalidCommand
+        exception is raised, if multiple matches are possible an
+        AmbiguousCommand exception is raised.
+        """
+        namemap = dict((k,k) for k in self._table)
+        for key in self._table:
+            for alias in self._table[key].aliaslist():
+                namemap[alias] = name
+        if name in namemap:
+            key = namemap[name]
+            return self._table[key]
+        # search partials
+        canidates = set()
+        for alias in namemap:
+            if alias.startswith(name):
+                canidates.add(alias)
+        if len(canidates) == 1:
+            key = namemap[canidates[0]]
+            return self._table[key]
+        if not canidates:
+            raise InvalidCommand(name)
+        else:
+            raise AmbiguousCommand(canidates)
+
+    # TODO:  "add" decorator
+        
+    
+
+'''
+
+
+def splitidents(idents):
+    """Return a ident string as a list of idents.
+    """
+    return str(idents).split()
+
+class CommandTable(object):
+    """Processes a table of commands and the global options.
+    """
+    def __init__(self, globalopts, commands):
+        self._global = OptionTable(globalopts)
+        self._table = {}
+        for command in commands:
+            self._addcommand(command)
+
+    def _addcommand(self, command):
+        func, idents, opts, desc = command
+        for ident in splitidents(idents):
+            self._table[ident] = (
+                func,
+                idents,
+                OptionTable(opts),
+                desc)
+
+    def labels(self):
+        return self._table.keys()
+
+
+    def firstlabels(self):
+        seen = []
+        for key in self.labels():
+            f, idents, opts, desc = self._table[key]
+            idents = splitidents(idents)
+            if idents[0] not in seen:
+                seen.append(idents[0])
+        return seen
+
+
+    def opts(self):
+        """Return the global options table
+        """
+        return self._global
+
+    def find(self, label):
+        """Given a command name or partial command name,
+        return the closest full command name.
+        If there are no matches a InvalidCommand exception is
+        raised. If there are too many matches an AmbiguousCommand
+        exception is raised.
+        """
+        matches = []
+        for key in self._table.keys():
+            if key.startswith(label):
+                matches.append(key)
+        if len(matches) == 1:
+            return matches[0]
+        if not matches:
+            raise InvalidCommand(label)
+        if label in self._table.keys():
+            #exact match
+            return label
+        raise AmbiguousCommand(label, matches)
+
+    def lookup(self, label):
+        """Return the table entry given a full or partial
+        command name.
+        """
+        key = self.find(label)
+        return self._table[key]
+
+    def get(self, label):
+        """For a full or partial command name, return the
+        function, it's identifiers, an options table containing
+        both command and global options and the command
+        description.
+        """
+        (func, id, options, desc) = self.lookup(label)
+        options = self._global + options
+        return (func, id, options, desc)
+
+
+def parsecommand(opttable, cmdtable, arguments, getopts=None):
+    """Parse a command line into an command function, an
+    options dict and an arguments list.
+    """
+    if getopts is None:
+        getopts = (_getopt.getopt, _getopt.gnu_getopt)
+    cmds = CommandTable(opttable, cmdtable)
+    short, long = (cmds.opts().shortoptspec(), cmds.opts().longoptspec())
+    try:
+        raw, args = getopts[0](arguments, short, long)
+    except _getopt.GetoptError, ee:
+        raise InvalidOption(str(ee))
+    if not args:
+        return (None, cmds.opts().assemble(raw), [])
+    label, args = args[0], args[1:]
+    (func, id, opts, desc) = cmds.get(label)
+    raw2, args = getopts[1](args, opts.shortoptspec(), opts.longoptspec())
+    return (func, opts.assemble(raw+raw2), args)
+    
+
+def listcommands(cmdtable):
+    cmds = CommandTable([], cmdtable)
+    disp = list(cmds.firstlabels())
+    disp.sort()
+    for label in disp:
+        desc = cmds.get(label)[3]
+        yield (label, desc)
+'''