diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2018-03-16 20:23:27 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-16 20:23:27 -0400 |
commit | f0429e3f96a11572abcd07248730da8219b40c5c (patch) | |
tree | bca90446f696fca6734ed991b96018c0c129c586 | |
parent | 40ccd3fde8b2d252dd10b65e3904b3429b0bfb6f (diff) | |
parent | d590116b0a9efdb874713a0755cc362bd898112a (diff) | |
download | cmd2-git-f0429e3f96a11572abcd07248730da8219b40c5c.tar.gz |
Merge pull request #316 from python-cmd2/sub_complete_tests
Sub complete tests
-rw-r--r-- | docs/argument_processing.rst | 6 | ||||
-rwxr-xr-x | examples/subcommands.py | 37 | ||||
-rw-r--r-- | tests/test_argparse.py | 91 |
3 files changed, 122 insertions, 12 deletions
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst index 957c842a..15c947fb 100644 --- a/docs/argument_processing.rst +++ b/docs/argument_processing.rst @@ -214,7 +214,13 @@ Sub-commands Sub-commands are supported for commands using either the ``@with_argparser`` or ``@with_argparser_and_unknown_args`` decorator. The syntax for supporting them is based on argparse sub-parsers. +Also, a convenience function called ``cmd_with_subs_completer`` is available to easily add tab completion to functions +that implement subcommands. By setting this as the completer of the base command function, the correct completer for +the chosen subcommand will be called. + See the subcommands_ example to learn more about how to use sub-commands in your ``cmd2`` application. +This example also demonstrates usage of ``cmd_with_subs_completer``. In addition, the docstring for +``cmd_with_subs_completer`` offers more details. .. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py diff --git a/examples/subcommands.py b/examples/subcommands.py index 59ebe4cb..fa99f6b4 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -1,15 +1,17 @@ #!/usr/bin/env python # coding=utf-8 -"""A simple example demonstrating how to use Argparse to support sub-commands. +"""A simple example demonstrating how to use Argparse to support subcommands. This example shows an easy way for a single command to have many subcommands, each of which takes different arguments and provides separate contextual help. """ import argparse +import functools +import sys import cmd2 -from cmd2 import with_argparser +from cmd2 import with_argparser, index_based_complete class SubcommandsExample(cmd2.Cmd): @@ -18,7 +20,7 @@ class SubcommandsExample(cmd2.Cmd): def __init__(self): cmd2.Cmd.__init__(self) - # sub-command functions for the base command + # subcommand functions for the base command def base_foo(self, args): """foo subcommand of base command""" self.poutput(args.x * args.y) @@ -27,31 +29,52 @@ class SubcommandsExample(cmd2.Cmd): """bar subcommand of base command""" self.poutput('((%s))' % args.z) + def base_sport(self, args): + """sport subcommand of base command""" + self.poutput('Sport is {}'.format(args.sport)) + + # noinspection PyUnusedLocal + def complete_base_sport(self, text, line, begidx, endidx): + """ Adds tab completion to base sport subcommand """ + sports = ['Football', 'Hockey', 'Soccer', 'Baseball'] + index_dict = {1: sports} + return index_based_complete(text, line, begidx, endidx, index_dict) + # 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 + # create the parser for the "foo" subcommand 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=base_foo) - # create the parser for the "bar" sub-command + # create the parser for the "bar" subcommand parser_bar = base_subparsers.add_parser('bar', help='bar help') parser_bar.add_argument('z', help='string') parser_bar.set_defaults(func=base_bar) + # create the parser for the "sport" subcommand + parser_sport = base_subparsers.add_parser('sport', help='sport help') + parser_sport.add_argument('sport', help='Enter name of a sport') + parser_sport.set_defaults(func=base_sport) + @with_argparser(base_parser) def do_base(self, args): """Base command help""" try: - # Call whatever sub-command function was selected + # Call whatever subcommand function was selected args.func(self, args) except AttributeError: - # No sub-command was provided, so as called + # No subcommand was provided, so as called self.do_help('base') + # functools.partialmethod was added in Python 3.4 + if sys.version_info >= (3, 4): + # This makes sure correct tab completion functions are called based on the selected subcommand + complete_base = functools.partialmethod(cmd2.Cmd.cmd_with_subs_completer, base='base') + if __name__ == '__main__': app = SubcommandsExample() diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 85b438e2..fb8836f3 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -3,9 +3,15 @@ Cmd2 testing for argument parsing """ import argparse +import functools import pytest +import readline +import sys import cmd2 +import mock +import six + from conftest import run_cmd, StdOut @@ -171,7 +177,7 @@ class SubcommandApp(cmd2.Cmd): def __init__(self): cmd2.Cmd.__init__(self) - # sub-command functions for the base command + # subcommand functions for the base command def base_foo(self, args): """foo subcommand of base command""" self.poutput(args.x * args.y) @@ -180,31 +186,52 @@ class SubcommandApp(cmd2.Cmd): """bar sucommand of base command""" self.poutput('((%s))' % args.z) + def base_sport(self, args): + """sport subcommand of base command""" + self.poutput('Sport is {}'.format(args.sport)) + + # noinspection PyUnusedLocal + def complete_base_sport(self, text, line, begidx, endidx): + """ Adds tab completion to base sport subcommand """ + sports = ['Football', 'Hockey', 'Soccer', 'Baseball'] + index_dict = {1: sports} + return cmd2.index_based_complete(text, line, begidx, endidx, index_dict) + # 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 + # create the parser for the "foo" subcommand 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=base_foo) - # create the parser for the "bar" sub-command + # create the parser for the "bar" subcommand parser_bar = base_subparsers.add_parser('bar', help='bar help') parser_bar.add_argument('z', help='string') parser_bar.set_defaults(func=base_bar) + # create the parser for the "sport" subcommand + parser_sport = base_subparsers.add_parser('sport', help='sport help') + parser_sport.add_argument('sport', help='Enter name of a sport') + parser_sport.set_defaults(func=base_sport) + @cmd2.with_argparser_and_unknown_args(base_parser) def do_base(self, args, arglist): """Base command help""" try: - # Call whatever sub-command function was selected + # Call whatever subcommand function was selected args.func(self, args) except AttributeError: - # No sub-command was provided, so as called + # No subcommand was provided, so as called self.do_help('base') + # functools.partialmethod was added in Python 3.4 + if six.PY3: + # This makes sure correct tab completion functions are called based on the selected subcommand + complete_base = functools.partialmethod(cmd2.Cmd.cmd_with_subs_completer, base='base') + @pytest.fixture def subcommand_app(): app = SubcommandApp() @@ -245,3 +272,57 @@ def test_subcommand_invalid_help(subcommand_app): out = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('usage: base') assert out[1].startswith("base: error: invalid choice: 'baz'") + +@pytest.mark.skipif(sys.version_info < (3,0), reason="functools.partialmethod requires Python 3.4+") +def test_subcommand_tab_completion(subcommand_app): + # This makes sure the correct completer for the sport subcommand is called + text = 'Foot' + line = 'base sport Foot' + endidx = len(line) + begidx = endidx - len(text) + state = 0 + + def get_line(): + return line + + def get_begidx(): + return begidx + + def get_endidx(): + return endidx + + with mock.patch.object(readline, 'get_line_buffer', get_line): + with mock.patch.object(readline, 'get_begidx', get_begidx): + with mock.patch.object(readline, 'get_endidx', get_endidx): + # Run the readline tab-completion function with readline mocks in place + first_match = subcommand_app.complete(text, state) + + # It is at end of line, so extra space is present + assert first_match is not None and subcommand_app.completion_matches == ['Football '] + +@pytest.mark.skipif(sys.version_info < (3,0), reason="functools.partialmethod requires Python 3.4+") +def test_subcommand_tab_completion_with_no_completer(subcommand_app): + # This tests what happens when a subcommand has no completer + # In this case, the foo subcommand has no completer defined + text = 'Foot' + line = 'base foo Foot' + endidx = len(line) + begidx = endidx - len(text) + state = 0 + + def get_line(): + return line + + def get_begidx(): + return begidx + + def get_endidx(): + return endidx + + with mock.patch.object(readline, 'get_line_buffer', get_line): + with mock.patch.object(readline, 'get_begidx', get_begidx): + with mock.patch.object(readline, 'get_endidx', get_endidx): + # Run the readline tab-completion function with readline mocks in place + first_match = subcommand_app.complete(text, state) + + assert first_match is None |