diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2018-03-28 02:14:29 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2018-03-28 02:14:29 -0400 |
commit | d78dc46a1a767427baa2b76215e235a7fec28b9a (patch) | |
tree | d905cab5030c8655a529d59e8dbf8cecf72131c4 | |
parent | 554561b70f899ad8ec38393b27eed646456cab62 (diff) | |
download | cmd2-git-d78dc46a1a767427baa2b76215e235a7fec28b9a.tar.gz |
Simplified how to add tab completion to a subcommand
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rwxr-xr-x | cmd2.py | 86 | ||||
-rwxr-xr-x | examples/subcommands.py | 17 | ||||
-rw-r--r-- | tests/test_completion.py | 22 |
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 @@ -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 |