-
Notifications
You must be signed in to change notification settings - Fork 127
argparse Autocompleter integration into cmd2 #366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
85c2c6b
965fa83
967f320
102fc67
cbb94bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,7 @@ | |
|
|
||
| # Set up readline | ||
| from .rl_utils import rl_force_redisplay, readline, rl_type, RlType | ||
| from .argparse_completer import AutoCompleter, ACArgumentParser | ||
|
|
||
| if rl_type == RlType.PYREADLINE: | ||
|
|
||
|
|
@@ -266,23 +267,8 @@ def cmd_wrapper(instance, cmdline): | |
|
|
||
| cmd_wrapper.__doc__ = argparser.format_help() | ||
|
|
||
| # Mark this function as having an argparse ArgumentParser (used by do_help) | ||
| cmd_wrapper.__dict__['has_parser'] = True | ||
|
|
||
| # If there are subcommands, store their names in a list to support tab-completion of subcommand names | ||
| if argparser._subparsers is not None: | ||
| # 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 | ||
| # Mark this function as having an argparse ArgumentParser | ||
| setattr(cmd_wrapper, 'argparser', argparser) | ||
|
|
||
| return cmd_wrapper | ||
|
|
||
|
|
@@ -318,24 +304,8 @@ def cmd_wrapper(instance, cmdline): | |
|
|
||
| cmd_wrapper.__doc__ = argparser.format_help() | ||
|
|
||
| # Mark this function as having an argparse ArgumentParser (used by do_help) | ||
| cmd_wrapper.__dict__['has_parser'] = True | ||
|
|
||
| # If there are subcommands, store their names in a list to support tab-completion of subcommand names | ||
| if argparser._subparsers is not None: | ||
|
|
||
| # 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 | ||
| # Mark this function as having an argparse ArgumentParser | ||
| setattr(cmd_wrapper, 'argparser', argparser) | ||
|
|
||
| return cmd_wrapper | ||
|
|
||
|
|
@@ -1020,49 +990,6 @@ def colorize(self, val, color): | |
| return self._colorcodes[color][True] + val + self._colorcodes[color][False] | ||
| return val | ||
|
|
||
| def get_subcommands(self, 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 | ||
| """ | ||
|
|
||
| subcommand_names = 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: | ||
| 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): | ||
|
|
@@ -1794,16 +1721,14 @@ def complete(self, text, state): | |
| try: | ||
| compfunc = getattr(self, 'complete_' + command) | ||
| except AttributeError: | ||
| compfunc = self.completedefault | ||
|
|
||
| subcommands = self.get_subcommands(command) | ||
| if subcommands is not None: | ||
| # Since there are subcommands, then try completing those if the cursor is in | ||
| # the token at index 1, otherwise default to using compfunc | ||
| index_dict = {1: subcommands} | ||
| compfunc = functools.partial(self.index_based_complete, | ||
| index_dict=index_dict, | ||
| all_else=compfunc) | ||
| # There's no completer function, next see if the command uses argparser | ||
| try: | ||
| cmd_func = getattr(self, 'do_' + command) | ||
| argparser = getattr(cmd_func, 'argparser') | ||
| # Command uses argparser, switch to the default argparse completer | ||
| compfunc = functools.partial(self._autocomplete_default, argparser=argparser) | ||
| except AttributeError: | ||
| compfunc = self.completedefault | ||
|
|
||
| # A valid command was not entered | ||
| else: | ||
|
|
@@ -1910,6 +1835,16 @@ def complete(self, text, state): | |
| except IndexError: | ||
| return None | ||
|
|
||
| def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int, | ||
| argparser: argparse.ArgumentParser) -> List[str]: | ||
| """Default completion function for argparse commands.""" | ||
| completer = AutoCompleter(argparser) | ||
|
|
||
| tokens, _ = self.tokens_for_completion(line, begidx, endidx) | ||
| results = completer.complete_command(tokens, text, line, begidx, endidx) | ||
|
|
||
| return results | ||
|
|
||
| def get_all_commands(self): | ||
| """ | ||
| Returns a list of all commands | ||
|
|
@@ -1964,12 +1899,15 @@ def complete_help(self, text, line, begidx, endidx): | |
| strs_to_match = list(topics | visible_commands) | ||
| matches = self.basic_complete(text, line, begidx, endidx, strs_to_match) | ||
|
|
||
| # Check if we are completing a subcommand | ||
| elif index == subcmd_index: | ||
|
|
||
| # Match subcommands if any exist | ||
| command = tokens[cmd_index] | ||
| matches = self.basic_complete(text, line, begidx, endidx, self.get_subcommands(command)) | ||
| # check if the command uses argparser | ||
| elif index >= subcmd_index: | ||
| try: | ||
| cmd_func = getattr(self, 'do_' + tokens[cmd_index]) | ||
| parser = getattr(cmd_func, 'argparser') | ||
| completer = AutoCompleter(parser) | ||
| matches = completer.complete_command_help(tokens[1:], text, line, begidx, endidx) | ||
| except AttributeError: | ||
| pass | ||
|
|
||
| return matches | ||
|
|
||
|
|
@@ -2620,7 +2558,7 @@ def do_help(self, arglist): | |
| if funcname: | ||
| # Check to see if this function was decorated with an argparse ArgumentParser | ||
| func = getattr(self, funcname) | ||
| if func.__dict__.get('has_parser', False): | ||
| if hasattr(func, 'argparser'): | ||
| # Function has an argparser, so get help based on all the arguments in case there are sub-commands | ||
| new_arglist = arglist[1:] | ||
| new_arglist.append('-h') | ||
|
|
@@ -2843,10 +2781,10 @@ def show(self, args, parameter): | |
| else: | ||
| raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param) | ||
|
|
||
| set_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) | ||
| set_parser = ACArgumentParser(formatter_class=argparse.RawTextHelpFormatter) | ||
| set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well') | ||
| set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter') | ||
| set_parser.add_argument('settable', nargs='*', help='[param_name] [value]') | ||
| set_parser.add_argument('settable', nargs=(0,2), help='[param_name] [value]') | ||
|
|
||
| @with_argparser(set_parser) | ||
| def do_set(self, args): | ||
|
|
@@ -2927,87 +2865,6 @@ def complete_shell(self, text, line, begidx, endidx): | |
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, this function had the shortest lifespan of anything I've ever written!!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it makes you feel any better - with the revelation that I can just tag the completer on each argparse argument action object I think can remove all of the complicated nested dictionary stuff that I did to pass that over to the AutoCompleter. |
||
| """ | ||
| 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 assigning a completer function to the subcommand's parser | ||
| Example: | ||
| 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 the names completer gets called, set the completer for the print function | ||
| in a similar fashion to what follows. | ||
|
|
||
| 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. | ||
|
|
||
| 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 | ||
| :return: List[str] - a 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 | ||
|
|
||
| # Get all tokens through the one being completed | ||
| tokens, _ = self.tokens_for_completion(line, begidx, endidx) | ||
| if tokens is None: | ||
| return [] | ||
|
|
||
| matches = [] | ||
|
|
||
| # Get the index of the token being completed | ||
| index = len(tokens) - 1 | ||
|
|
||
| # 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] | ||
|
|
||
| # Find the offset into line where the subcommand name begins | ||
| subcmd_start = 0 | ||
| for cur_index in range(0, subcmd_index + 1): | ||
| cur_token = tokens[cur_index] | ||
| subcmd_start = line.find(cur_token, subcmd_start) | ||
|
|
||
| if cur_index != subcmd_index: | ||
| subcmd_start += len(cur_token) | ||
|
|
||
| # Strip off everything before subcommand name | ||
| orig_line = line | ||
| line = line[subcmd_start:] | ||
|
|
||
| # Update the indexes | ||
| diff = len(orig_line) - len(line) | ||
| begidx -= diff | ||
| endidx -= diff | ||
|
|
||
| # Call the subcommand specific completer if it exists | ||
| compfunc = self.get_subcommand_completer(command, subcommand) | ||
| if compfunc is not None: | ||
| matches = compfunc(self, text, line, begidx, endidx) | ||
|
|
||
| return matches | ||
|
|
||
| # noinspection PyBroadException | ||
| def do_py(self, arg): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -346,12 +346,10 @@ 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. | ||
| You may add multiple layers of sub-commands for your command. Cmd2 will automatically traverse and tab-complete | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for updating the docs |
||
| sub-commands for all commands using argparse. | ||
|
|
||
| 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. | ||
| See the subcommands_ and tab_autocompletion_ example to learn more about how to use sub-commands in your ``cmd2`` application. | ||
|
|
||
| .. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py | ||
| .. _tab_autocompletion: https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this. It makes it easier for the vast majority of cases.