diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rwxr-xr-x | README.md | 2 | ||||
-rwxr-xr-x | cmd2.py | 2 | ||||
-rwxr-xr-x | examples/subcommands.py | 4 | ||||
-rw-r--r-- | tests/test_argparse.py | 92 | ||||
-rw-r--r-- | tests/test_completion.py | 97 |
6 files changed, 194 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d7dfbcf3..83ca8773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples * Added support for Argpasre sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators * See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands + * Tab-completion of sub-command names is automatically supported * The **__relative_load** command is now hidden from the help menu by default * This command is not intended to be called from the command line, only from within scripts * The **set** command now has an additional **-a/--all** option to also display read-only settings @@ -31,7 +31,7 @@ Main Features - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands - Unicode character support (*Python 3 only*) -- Good tab-completion of commands, file system paths, and shell commands +- Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support - Windows, macOS, and Linux support - Trivial to provide built-in help for all commands @@ -776,7 +776,7 @@ class Cmd(cmd.Cmd): cmd, args, foo = self.parseline(line) arglist = args.split() - if cmd + ' ' + args == line: + if len(arglist) <= 1 and cmd + ' ' + args == line: funcname = self._func_named(cmd) if funcname: # Check to see if this function was decorated with an argparse ArgumentParser diff --git a/examples/subcommands.py b/examples/subcommands.py index eaadbb64..67b06e18 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -21,11 +21,11 @@ class SubcommandsExample(cmd2.Cmd): # sub-command functions for the base command def foo(self, args): """foo subcommand of base command""" - print(args.x * args.y) + self.poutput(args.x * args.y) def bar(self, args): """bar sucommand of base command""" - print('((%s))' % args.z) + self.poutput('((%s))' % args.z) # create the top-level parser for the base command base_parser = argparse.ArgumentParser(prog='base') diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 733e741b..bb494d49 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -8,6 +8,7 @@ import pytest import cmd2 from conftest import run_cmd, StdOut + class ArgparseApp(cmd2.Cmd): def __init__(self): self.maxrepeats = 3 @@ -168,3 +169,94 @@ def test_arglist(argparse_app): def test_arglist_decorator_twice(argparse_app): out = run_cmd(argparse_app, 'arglisttwice "we should" get these') assert out[0] == 'we should get these' + + +class SubcommandApp(cmd2.Cmd): + """ Example cmd2 application where we a base command which has a couple subcommands.""" + + def __init__(self): + cmd2.Cmd.__init__(self) + + # sub-command functions for the base command + def foo(self, args): + """foo subcommand of base command""" + self.poutput(args.x * args.y) + + def bar(self, args): + """bar sucommand of base command""" + self.poutput('((%s))' % args.z) + + # create the top-level parser for the base command + base_parser = argparse.ArgumentParser(prog='base') + base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" sub-command + parser_foo = base_subparsers.add_parser('foo', help='foo help') + parser_foo.add_argument('-x', type=int, default=1, help='integer') + parser_foo.add_argument('y', type=float, help='float') + parser_foo.set_defaults(func=foo) + + # create the parser for the "bar" sub-command + parser_bar = base_subparsers.add_parser('bar', help='bar help') + parser_bar.add_argument('z', help='string') + parser_bar.set_defaults(func=bar) + + # Create a list of subcommand names, which is used to enable tab-completion of sub-commands + subcommands = ['foo', 'bar'] + + @cmd2.with_argparser_and_unknown_args(base_parser, subcommands) + def do_base(self, args, arglist): + """Base command help""" + try: + # Call whatever sub-command function was selected + args.func(self, args) + except AttributeError: + # No sub-command was provided, so as called + self.do_help('base') + +@pytest.fixture +def subcommand_app(): + app = SubcommandApp() + app.stdout = StdOut() + return app + + +def test_subcommand_foo(subcommand_app): + out = run_cmd(subcommand_app, 'base foo -x2 5.0') + assert out == ['10.0'] + + +def test_subcommand_bar(subcommand_app): + out = run_cmd(subcommand_app, 'base bar baz') + assert out == ['((baz))'] + +def test_subcommand_invalid(subcommand_app, capsys): + run_cmd(subcommand_app, 'base baz') + out, err = capsys.readouterr() + err = err.splitlines() + assert err[0].startswith('usage: base') + assert err[1].startswith("base: error: invalid choice: 'baz'") + +def test_subcommand_base_help(subcommand_app, capsys): + run_cmd(subcommand_app, 'help base') + out, err = capsys.readouterr() + out = out.splitlines() + assert out[0].startswith('usage: base') + assert out[1] == '' + assert out[2] == 'Base command help' + +def test_subcommand_help(subcommand_app, capsys): + run_cmd(subcommand_app, 'help base foo') + out, err = capsys.readouterr() + out = out.splitlines() + assert out[0].startswith('usage: base foo') + assert out[1] == '' + assert out[2] == 'positional arguments:' + + +def test_subcommand_invalid_help(subcommand_app, capsys): + run_cmd(subcommand_app, 'help base baz') + out, err = capsys.readouterr() + err = err.splitlines() + assert err[0].startswith('usage: base') + assert err[1].startswith("base: error: invalid choice: 'baz'") diff --git a/tests/test_completion.py b/tests/test_completion.py index efc32986..a0ee503c 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -8,6 +8,7 @@ file system paths, and shell commands. Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com> Released under MIT license, see LICENSE file """ +import argparse import os import sys @@ -323,3 +324,99 @@ def test_parseline_expands_shortcuts(cmd2_app): assert command == 'shell' assert args == 'cat foobar.txt' assert line.replace('!', 'shell ') == out_line + + +class SubcommandsExample(cmd2.Cmd): + """ Example cmd2 application where we a base command which has a couple subcommands.""" + + def __init__(self): + cmd2.Cmd.__init__(self) + + # sub-command functions for the base command + def foo(self, args): + """foo subcommand of base command""" + self.poutput(args.x * args.y) + + def bar(self, args): + """bar sucommand of base command""" + self.poutput('((%s))' % args.z) + + # create the top-level parser for the base command + base_parser = argparse.ArgumentParser(prog='base') + base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" sub-command + parser_foo = base_subparsers.add_parser('foo', help='foo help') + parser_foo.add_argument('-x', type=int, default=1, help='integer') + parser_foo.add_argument('y', type=float, help='float') + parser_foo.set_defaults(func=foo) + + # create the parser for the "bar" sub-command + parser_bar = base_subparsers.add_parser('bar', help='bar help') + parser_bar.add_argument('z', help='string') + parser_bar.set_defaults(func=bar) + + # Create a list of subcommand names, which is used to enable tab-completion of sub-commands + subcommands = ['foo', 'bar'] + + @cmd2.with_argument_parser(base_parser, subcommands) + def do_base(self, args): + """Base command help""" + try: + # Call whatever sub-command function was selected + args.func(self, args) + except AttributeError: + # No sub-command was provided, so as called + self.do_help('base') + + +@pytest.fixture +def sc_app(): + app = SubcommandsExample() + return app + + +def test_cmd2_subcommand_completion_single_end(sc_app): + text = 'f' + line = 'base f' + endidx = len(line) + begidx = endidx - len(text) + + # It is at end of line, so extra space is present + assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo '] + +def test_cmd2_subcommand_completion_single_mid(sc_app): + text = 'f' + line = 'base f' + endidx = len(line) - 1 + begidx = endidx - len(text) + + # It is at end of line, so extra space is present + assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo'] + +def test_cmd2_subcommand_completion_multiple(sc_app): + text = '' + line = 'base ' + endidx = len(line) + begidx = endidx - len(text) + + # It is at end of line, so extra space is present + assert sc_app.complete_subcommand(text, line, begidx, endidx) == ['foo', 'bar'] + +def test_cmd2_subcommand_completion_nomatch(sc_app): + text = 'z' + line = 'base z' + endidx = len(line) + begidx = endidx - len(text) + + # It is at end of line, so extra space is present + assert sc_app.complete_subcommand(text, line, begidx, endidx) == [] + +def test_cmd2_subcommand_completion_after_subcommand(sc_app): + text = 'f' + line = 'base foo f' + endidx = len(line) + begidx = endidx - len(text) + + # It is at end of line, so extra space is present + assert sc_app.complete_subcommand(text, line, begidx, endidx) == [] |