summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-03-16 20:23:27 -0400
committerGitHub <noreply@github.com>2018-03-16 20:23:27 -0400
commitf0429e3f96a11572abcd07248730da8219b40c5c (patch)
treebca90446f696fca6734ed991b96018c0c129c586
parent40ccd3fde8b2d252dd10b65e3904b3429b0bfb6f (diff)
parentd590116b0a9efdb874713a0755cc362bd898112a (diff)
downloadcmd2-git-f0429e3f96a11572abcd07248730da8219b40c5c.tar.gz
Merge pull request #316 from python-cmd2/sub_complete_tests
Sub complete tests
-rw-r--r--docs/argument_processing.rst6
-rwxr-xr-xexamples/subcommands.py37
-rw-r--r--tests/test_argparse.py91
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