summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2018-03-28 02:14:29 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2018-03-28 02:14:29 -0400
commitd78dc46a1a767427baa2b76215e235a7fec28b9a (patch)
treed905cab5030c8655a529d59e8dbf8cecf72131c4
parent554561b70f899ad8ec38393b27eed646456cab62 (diff)
downloadcmd2-git-d78dc46a1a767427baa2b76215e235a7fec28b9a.tar.gz
Simplified how to add tab completion to a subcommand
-rw-r--r--CHANGELOG.md6
-rwxr-xr-xcmd2.py86
-rwxr-xr-xexamples/subcommands.py17
-rw-r--r--tests/test_completion.py22
4 files changed, 91 insertions, 40 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c130ac27..6d3b0c19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@
* ``exclude_from_help`` is now called ``hidden_commands`` since these commands are hidden from things other than help, including tab completion
* This list also no longer takes the function names of commands (``do_history``), but instead uses the command names themselves (``history``)
* ``excludeFromHistory`` is now called ``exclude_from_history``
+ * ``cmd_with_subs_completer()`` no longer takes an argument called ``base``. Adding tab completion to subcommands has been simplified to declaring it in the
+ subcommand parser's default settings. This easily allows arbitrary completers like path_complete to be used.
+ See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use
+ tab completion in subcommands. In addition, the docstring for ``cmd_with_subs_completer()`` offers more details.
## 0.8.2 (March 21, 2018)
@@ -75,7 +79,7 @@
* See the [Argument Processing](http://cmd2.readthedocs.io/en/latest/argument_processing.html) section of the documentation for more information on these decorators
* Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py)
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
+ * Added support for Argparse 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
diff --git a/cmd2.py b/cmd2.py
index 712a073b..fa8c402d 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -430,8 +430,19 @@ def with_argparser(argparser):
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
if argparser._subparsers is not None:
- subcommand_names = argparser._subparsers._group_actions[0]._name_parser_map.keys()
- cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
+
+ # Key is subcommand name and value is completer function
+ subcommands = collections.OrderedDict()
+
+ # Get all subcommands and check if they have completer functions
+ for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
+ if 'completer' in parser._defaults:
+ completer = parser._defaults['completer']
+ else:
+ completer = None
+ subcommands[name] = completer
+
+ cmd_wrapper.__dict__['subcommands'] = subcommands
return cmd_wrapper
@@ -1214,8 +1225,8 @@ class Cmd(cmd.Cmd):
def get_subcommands(self, command):
"""
- Returns a list of a command's subcommands if they exist
- :param command:
+ Returns a list of a command's subcommand names if they exist
+ :param command: the command we are querying
:return: A subcommand list or None
"""
@@ -1227,10 +1238,34 @@ class Cmd(cmd.Cmd):
if funcname:
# Check to see if this function was decorated with an argparse ArgumentParser
func = getattr(self, funcname)
- subcommand_names = func.__dict__.get('subcommand_names', None)
+ subcommands = func.__dict__.get('subcommands', None)
+ if subcommands is not None:
+ subcommand_names = subcommands.keys()
return subcommand_names
+ def get_subcommand_completer(self, command, subcommand):
+ """
+ Returns a subcommand's tab completion function if one exists
+ :param command: command which owns the subcommand
+ :param subcommand: the subcommand we are querying
+ :return: A completer or None
+ """
+
+ completer = None
+
+ # Check if is a valid command
+ funcname = self._func_named(command)
+
+ if funcname:
+ # Check to see if this function was decorated with an argparse ArgumentParser
+ func = getattr(self, funcname)
+ subcommands = func.__dict__.get('subcommands', None)
+ if subcommands is not None:
+ completer = subcommands[subcommand]
+
+ return completer
+
# ----- Methods related to tab completion -----
def set_completion_defaults(self):
@@ -3069,40 +3104,40 @@ Usage: Usage: unalias [-a] name [name ...]
index_dict = {1: self.shell_cmd_complete}
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
- def cmd_with_subs_completer(self, text, line, begidx, endidx, base):
+ def cmd_with_subs_completer(self, text, line, begidx, endidx):
"""
This is a function provided for convenience to those who want an easy way to 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.
- The use of this function requires a particular naming scheme.
+ The use of this function requires assigning a completer function to the subcommand's parser
Example:
- A command called print has 2 subcommands [names, addresses]
- The tab-completion functions for the subcommands must be called:
- names -> complete_print_names
- addresses -> complete_print_addresses
+ A command called print has a subcommands called 'names' that needs a tab completer
+ When you create the parser for names, include the completer function in the parser's defaults.
+
+ names_parser.set_defaults(func=print_names, completer=complete_print_names)
- To make sure these functions get called, set the tab-completer for the print function
- in a similar fashion to what follows where base is the name of the root command (print)
+ To make sure the names completer gets called, set the completer for the print function
+ in a similar fashion to what follows.
- def complete_print(self, text, line, begidx, endidx):
- return self.cmd_with_subs_completer(text, line, begidx, endidx, base='print')
+ complete_print = cmd2.Cmd.cmd_with_subs_completer
- When the subcommand's completer is called, this function will have stripped off all content from the
- beginning of the command line before the subcommand, meaning the line parameter always starts with the
- subcommand name and the index parameters reflect this change.
+ When the subcommand's completer is called, this function will have stripped off all content from the
+ beginning of the command line before the subcommand, meaning the line parameter always starts with the
+ subcommand name and the index parameters reflect this change.
- For instance, the command "print names -d 2" becomes "names -d 2"
- begidx and endidx are incremented accordingly
+ For instance, the command "print names -d 2" becomes "names -d 2"
+ begidx and endidx are incremented accordingly
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
:param line: str - the current input line with leading whitespace removed
:param begidx: int - the beginning index of the prefix text
:param endidx: int - the ending index of the prefix text
- :param base: str - the name of the base command that owns the subcommands
:return: List[str] - a sorted list of possible tab completions
"""
+ # The command is the token at index 0 in the command line
+ cmd_index = 0
# The subcommand is the token at index 1 in the command line
subcmd_index = 1
@@ -3120,6 +3155,9 @@ Usage: Usage: unalias [-a] name [name ...]
# If the token being completed is past the subcommand name, then do subcommand specific tab-completion
if index > subcmd_index:
+ # Get the command name
+ command = tokens[cmd_index]
+
# Get the subcommand name
subcommand = tokens[subcmd_index]
@@ -3142,11 +3180,9 @@ Usage: Usage: unalias [-a] name [name ...]
endidx -= diff
# Call the subcommand specific completer if it exists
- completer = 'complete_{}_{}'.format(base, subcommand)
- compfunc = getattr(self, completer, None)
-
+ compfunc = self.get_subcommand_completer(command, subcommand)
if compfunc is not None:
- matches = compfunc(text, line, begidx, endidx)
+ matches = compfunc(self, text, line, begidx, endidx)
return matches
diff --git a/examples/subcommands.py b/examples/subcommands.py
index 7efed093..cbe4f634 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -11,9 +11,13 @@ import argparse
import cmd2
from cmd2 import with_argparser
+sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
class SubcommandsExample(cmd2.Cmd):
- """ Example cmd2 application where we a base command which has a couple subcommands."""
+ """
+ Example cmd2 application where we a base command which has a couple subcommands
+ and the "sport" subcommand has tab completion enabled.
+ """
def __init__(self):
cmd2.Cmd.__init__(self)
@@ -34,8 +38,7 @@ class SubcommandsExample(cmd2.Cmd):
# 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}
+ index_dict = {1: sport_item_strs}
return self.index_based_complete(text, line, begidx, endidx, index_dict)
# create the top-level parser for the base command
@@ -56,7 +59,9 @@ class SubcommandsExample(cmd2.Cmd):
# 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)
+
+ # Set both a function and tab completer for the "sport" subcommand
+ parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
@with_argparser(base_parser)
def do_base(self, args):
@@ -69,8 +74,8 @@ class SubcommandsExample(cmd2.Cmd):
# No subcommand was provided, so call help
self.do_help('base')
- def complete_base(self, text, line, begidx, endidx):
- return self.cmd_with_subs_completer(text, line, begidx, endidx, base='base')
+ # Enable tab completion of base to make sure the subcommands' completers get called.
+ complete_base = cmd2.Cmd.cmd_with_subs_completer
if __name__ == '__main__':
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 8c2fd55d..e779e44b 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -631,12 +631,15 @@ def test_parseline_expands_shortcuts(cmd2_app):
class SubcommandsExample(cmd2.Cmd):
- """ Example cmd2 application where we a base command which has a couple subcommands."""
+ """
+ Example cmd2 application where we a base command which has a couple subcommands
+ and the "sport" subcommand has tab completion enabled.
+ """
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)
@@ -649,6 +652,7 @@ class SubcommandsExample(cmd2.Cmd):
"""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 """
index_dict = {1: sport_item_strs}
@@ -658,13 +662,13 @@ class SubcommandsExample(cmd2.Cmd):
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)
@@ -672,7 +676,9 @@ class SubcommandsExample(cmd2.Cmd):
# 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)
+
+ # Set both a function and tab completer for the "sport" subcommand
+ parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
@cmd2.with_argparser(base_parser)
def do_base(self, args):
@@ -682,11 +688,11 @@ class SubcommandsExample(cmd2.Cmd):
# Call whatever subcommand function was selected
func(self, args)
else:
- # No sub-command was provided, so as called
+ # No subcommand was provided, so call help
self.do_help('base')
- def complete_base(self, text, line, begidx, endidx):
- return self.cmd_with_subs_completer(text, line, begidx, endidx, base='base')
+ # Enable tab completion of base to make sure the subcommands' completers get called.
+ complete_base = cmd2.Cmd.cmd_with_subs_completer
@pytest.fixture