# HG changeset patch # User John Mulligan # Date 1257025139 14400 # Sat Oct 31 17:38:59 2009 -0400 # Node ID d6ec6a714ac2143f9c2c33591f35f3ebc86ef872 # Parent b8e9867c9baee21e9a5fae3eeb40ab4e661d3d6b create new commander modules for commands, begin tests diff --git a/test/test_cli.py b/test/test_cli_commands.py rename from test/test_cli.py rename to test/test_cli_commands.py --- a/test/test_cli.py +++ b/test/test_cli_commands.py @@ -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__': diff --git a/vanity/cli.py b/vanity/cli.py --- a/vanity/cli.py +++ b/vanity/cli.py @@ -85,11 +85,18 @@ * ``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: diff --git a/vanity/commander.py b/vanity/commander.py new file mode 100644 --- /dev/null +++ b/vanity/commander.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# +# Copyright 2009 John Mulligan +# +# 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) +'''