# HG changeset patch # User John Mulligan # Date 1257113010 18000 # Sun Nov 01 17:03:30 2009 -0500 # Node ID 625e05b49e500afbb96fa7ca9c4353b003c82569 # Parent da03ff76887aa96357c0e59c9eb5ae576f9671c5 commander: test and fix fancy help handling diff --git a/test/test_cli_commands.py b/test/test_cli_commands.py --- a/test/test_cli_commands.py +++ b/test/test_cli_commands.py @@ -10,21 +10,25 @@ def cmd1(fn, **opts): + """the number one operation + + This produces the number one. Always. + """ return 1 def cmd2(**opts): return 2 -def cmd3(): +def cmd3(**opts): return 3 CMDTABLE_A = [ - ('foo', cmd1, '', [], 'foo fixes everything'), + ('foo', cmd1, '', [], 'FILENAME'), ('bar', cmd2, '', [], 'bar breaks it'), ('baz', cmd3, 'wibble|hamper', [ ('shrimp', '', None, 'jumbo shrimp'), - ], 'something helpful'), + ], ''), ] CMDTABLE_B = [ @@ -145,5 +149,71 @@ commander.launch, GOPT_A, CMDTABLE_A, arguments) + def test_simple_usage(self): + arguments = 'foo test.txt'.split() + cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments) + usage = '\n'.join(commander.usage(cmd)) + self.assert_('FILENAME' in usage) + self.assert_('number one' in usage) + self.assert_('Always' in usage) + + arguments = 'baz test.txt'.split() + cmd, opts, args = commander.parse(GOPT_A, CMDTABLE_A, arguments) + usage = '\n'.join(commander.usage(cmd)) + self.assert_('--shrimp' in usage) + self.assert_('jumbo shrimp' in usage) + + + def test_fancy_help_1(self): + arguments = 'foo -h'.split() + capture = Capture() + try: + result = commander.launch(GOPT_A, CMDTABLE_A, arguments) + except commander.HelpWanted, herr: + commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture) + usage = ''.join(capture) + self.assert_('number one' in usage) + + def test_fancy_help_2(self): + arguments = '-h'.split() + capture = Capture() + try: + result = commander.launch(GOPT_A, CMDTABLE_A, arguments) + except commander.HelpWanted, herr: + commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture) + usage = ''.join(capture) + self.assert_('foo' in usage) + self.assert_('bar' in usage) + self.assert_('baz' in usage) + + def test_fancy_help_3(self): + def tst(arguments): + capture = Capture() + try: + result = commander.launch(GOPT_A, CMDTABLE_A, arguments) + except commander.HelpWanted, herr: + commander.handlehelp(herr, GOPT_A, CMDTABLE_A, output=capture) + return ''.join(capture) + c1 = tst('foo --help'.split()) + c2 = tst('foo -h'.split()) + c3 = tst('--help foo'.split()) + c4 = tst('help foo'.split()) + self.assertEqual(c1, c2) + self.assertEqual(c2, c3) + self.assertEqual(c3, c4) + + c1 = tst('help'.split()) + c2 = tst('--help'.split()) + c3 = tst('-h'.split()) + self.assertEqual(c1, c2) + self.assertEqual(c2, c3) + + + +class Capture(list): + def write(self, x): + self.append(x) + + if __name__ == '__main__': unittest.main() diff --git a/vanity/commander.py b/vanity/commander.py --- a/vanity/commander.py +++ b/vanity/commander.py @@ -25,6 +25,9 @@ """The CLI was given an invalid command""" pass +class MissingCommand(cli.CliError): + """The user failed to specify a command""" + pass class AmbiguousCommand(cli.CliError): """The given command was ambigouous""" @@ -57,7 +60,13 @@ cmdtable._table[HELP.name] = HELP globalopts._table[HELPOPT.name] = HELPOPT # parse cli - cmd, opts, args = parse(globalopts, cmdtable, arguments) + try: + cmd, opts, args = parse(globalopts, cmdtable, arguments) + except MissingCommand: + cmd = HELP + opts, args = cli.parse(globalopts, arguments, strict=True) + if not opts.get('help'): + raise # if help requested: raise HelpWanted if cmd == HELP or opts.get('help'): raise HelpWanted(cmd, opts, args) @@ -77,12 +86,64 @@ cmds = CommandTable(cmdtable) opts, args = cli._parse(gopts, arguments, True) # TODO : handle missing command + if not args: + raise MissingCommand() 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 - + + +def handlehelp(helperr, globalopts, cmdtable, output=None, generic=None): + if output is None: + output = sys.stdout + if not generic: + generic = generic_usage + # determine what type of help we're getting + if helperr.cmd != HELP: + check = helperr.cmd.name + elif helperr.args: + check = helperr.args[0] + else: + check = None + # produce a help text iter + cmdtable = CommandTable(cmdtable) + try: + if check is None: + content = generic(cmdtable) + else: + content = usage(cmdtable.find(check)) + except cli.CliError, err: + content = ['error: %s' % err] + for line in content: + output.write('%s\n' % line) + + +def generic_usage(cmdtable): + yield 'Application Subcommands:' + yield '' + for cmd in sorted(cmdtable): + for line in usage(cmd, short=True): + yield ' %-12s %s' % (cmd.name, line) + + + +def usage(cmd, short=False, prefix=''): + if short: + yield cmd.docstring().splitlines()[0] + return + yield 'usage: %s%s [OPTIONS] %s' % (prefix, cmd.name, cmd.help) + doc = cmd.docstring().strip() + if doc: + yield '' + yield '%s' % doc + yield '' + if cmd.opts and list(cmd.opts.longopts()): + yield 'OPTIONS:' + for line in cli.usage(cmd.opts): + yield line + class Command(object): """A thin class representing a command. @@ -108,6 +169,15 @@ else: return [] + def docstring(self): + if self.target and hasattr(self.target, '__doc__'): + return self.target.__doc__ or 'No usage available' + else: + return 'No usage available' + + def __cmp__(self, other): + return cmp(self.name, other.name) + @classmethod def convert(cls, obj): if isinstance(obj, cls):