diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rwxr-xr-x | cmd2/argparse_completer.py | 44 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 215 | ||||
-rw-r--r-- | docs/argument_processing.rst | 10 | ||||
-rwxr-xr-x | examples/subcommands.py | 22 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 132 | ||||
-rw-r--r-- | tests/test_autocompletion.py | 23 | ||||
-rw-r--r-- | tests/test_cmd2.py | 8 | ||||
-rw-r--r-- | tests/test_completion.py | 89 |
9 files changed, 230 insertions, 315 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2e785f..bb577994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * All ``cmd2`` code should be ported to use the new ``argparse``-based decorators * 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) + * Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer`` + * Replaced by default AutoCompleter implementation for all commands using argparse * Python 2 no longer supported * ``cmd2`` now supports Python 3.4+ diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 35f9342b..03f2d965 100755 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -71,6 +71,10 @@ import re as _re from .rl_utils import rl_force_redisplay +# attribute that can optionally added to an argparse argument (called an Action) to +# define the completion choices for the argument. You may provide a Collection or a Function. +ACTION_ARG_CHOICES = 'arg_choices' + class _RangeAction(object): def __init__(self, nargs: Union[int, str, Tuple[int, int], None]): self.nargs_min = None @@ -186,7 +190,8 @@ class AutoCompleter(object): token_start_index: int = 1, arg_choices: Dict[str, Union[List, Tuple, Callable]] = None, subcmd_args_lookup: dict = None, - tab_for_arg_help: bool = True): + tab_for_arg_help: bool = True, + cmd2_app=None): """ Create an AutoCompleter @@ -195,6 +200,8 @@ class AutoCompleter(object): :param arg_choices: dictionary mapping from argparse argument 'dest' name to list of choices :param subcmd_args_lookup: mapping a sub-command group name to a tuple to fill the child\ AutoCompleter's arg_choices and subcmd_args_lookup parameters + :param tab_for_arg_help: Enable of disable argument help when there's no completion result + :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods """ if not subcmd_args_lookup: subcmd_args_lookup = {} @@ -205,6 +212,7 @@ class AutoCompleter(object): self._arg_choices = arg_choices.copy() if arg_choices is not None else {} self._token_start_index = token_start_index self._tab_for_arg_help = tab_for_arg_help + self._cmd2_app = cmd2_app self._flags = [] # all flags in this command self._flags_without_args = [] # all flags that don't take arguments @@ -220,6 +228,10 @@ class AutoCompleter(object): # if there are choices defined, record them in the arguments dictionary if action.choices is not None: self._arg_choices[action.dest] = action.choices + # if completion choices are tagged on the action, record them + elif hasattr(action, ACTION_ARG_CHOICES): + action_arg_choices = getattr(action, ACTION_ARG_CHOICES) + self._arg_choices[action.dest] = action_arg_choices # if the parameter is flag based, it will have option_strings if action.option_strings: @@ -244,7 +256,8 @@ class AutoCompleter(object): subcmd_start = token_start_index + len(self._positional_actions) sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], subcmd_start, arg_choices=subcmd_args, - subcmd_args_lookup=subcmd_lookup) + subcmd_args_lookup=subcmd_lookup, + cmd2_app=cmd2_app) sub_commands.append(subcmd) self._positional_completers[action.dest] = sub_completers self._arg_choices[action.dest] = sub_commands @@ -406,6 +419,21 @@ class AutoCompleter(object): return completion_results + def complete_command_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]: + """Supports the completion of sub-commands for commands through the cmd2 help command.""" + for idx, token in enumerate(tokens): + if idx >= self._token_start_index: + if self._positional_completers: + # For now argparse only allows 1 sub-command group per level + # so this will only loop once. + for completers in self._positional_completers.values(): + if token in completers: + return completers[token].complete_command_help(tokens, text, line, begidx, endidx) + else: + return self.basic_complete(text, line, begidx, endidx, completers.keys()) + return [] + + @staticmethod def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None: if isinstance(action, _RangeAction): @@ -467,8 +495,18 @@ class AutoCompleter(object): def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> List[str]: if action.dest in self._arg_choices: args = self._arg_choices[action.dest] + if callable(args): - args = args() + try: + if self._cmd2_app is not None: + try: + args = args(self._cmd2_app) + except TypeError: + args = args() + else: + args = args() + except TypeError: + return [] try: iter(args) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 7b01a653..43973b64 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -50,6 +50,7 @@ import pyperclip # Set up readline from .rl_utils import rl_force_redisplay, readline, rl_type, RlType +from .argparse_completer import AutoCompleter, ACArgumentParser from cmd2.parsing import CommandParser @@ -261,23 +262,8 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Calla 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 @@ -313,24 +299,8 @@ def with_argparser(argparser: argparse.ArgumentParser) -> Callable: 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 @@ -992,49 +962,6 @@ class Cmd(cmd.Cmd): 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): @@ -1766,16 +1693,14 @@ class Cmd(cmd.Cmd): 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: @@ -1882,6 +1807,16 @@ class Cmd(cmd.Cmd): 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, cmd2_app=self) + + 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 @@ -1936,12 +1871,15 @@ class Cmd(cmd.Cmd): 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 @@ -2104,7 +2042,7 @@ class Cmd(cmd.Cmd): :param line: str - line of text read from input :return: bool - True if cmdloop() should exit, False otherwise """ - stop = 0 + stop = False try: statement = self._complete_statement(line) (stop, statement) = self.postparsing_precmd(statement) @@ -2622,7 +2560,7 @@ Usage: Usage: unalias [-a] name [name ...] 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') @@ -2845,10 +2783,10 @@ Usage: Usage: unalias [-a] name [name ...] 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): @@ -2929,87 +2867,6 @@ 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): - """ - 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): diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst index 183dde4e..ecf59504 100644 --- a/docs/argument_processing.rst +++ b/docs/argument_processing.rst @@ -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 +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 diff --git a/examples/subcommands.py b/examples/subcommands.py index 031b17b2..75c0733e 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -35,12 +35,6 @@ 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} - return self.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') @@ -53,15 +47,22 @@ class SubcommandsExample(cmd2.Cmd): # 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) + bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') + parser_bar.add_argument('z', help='string') + + bar_subparsers.add_parser('apple', help='apple help') + bar_subparsers.add_parser('artichoke', help='artichoke help') + bar_subparsers.add_parser('cranberries', help='cranberries help') + # 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') + sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') + setattr(sport_arg, 'arg_choices', sport_item_strs) # Set both a function and tab completer for the "sport" subcommand - parser_sport.set_defaults(func=base_sport, completer=complete_base_sport) + parser_sport.set_defaults(func=base_sport) @with_argparser(base_parser) def do_base(self, args): @@ -74,9 +75,6 @@ class SubcommandsExample(cmd2.Cmd): # No subcommand was provided, so call help self.do_help('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__': app = SubcommandsExample() diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index c704908f..a1a8daee 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -13,6 +13,15 @@ from typing import List import cmd2 from cmd2 import with_argparser, with_category, argparse_completer +actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', + 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', + 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', + 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] + +def query_actors() -> List[str]: + """Simulating a function that queries and returns a completion values""" + return actors + class TabCompleteExample(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple subcommands.""" @@ -27,10 +36,6 @@ class TabCompleteExample(cmd2.Cmd): show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA'] static_list_directors = ['J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand', 'Rian Johnson', 'Gareth Edwards'] - actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', - 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', - 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', - 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05'] MOVIE_DATABASE_IDS = ['SW_EP01', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04', 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09'] @@ -90,6 +95,10 @@ class TabCompleteExample(cmd2.Cmd): }, } + def instance_query_actors(self) -> List[str]: + """Simulating a function that queries and returns a completion values""" + return actors + # This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser # - The help output will separately group required vs optional flags # - The help output for arguments with multiple flags or with append=True is more concise @@ -115,15 +124,6 @@ class TabCompleteExample(cmd2.Cmd): if not args.type: self.do_help('suggest') - def complete_suggest(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """ Adds tab completion to media""" - completer = argparse_completer.AutoCompleter(TabCompleteExample.suggest_parser, 1) - - tokens, _ = self.tokens_for_completion(line, begidx, endidx) - results = completer.complete_command(tokens, text, line, begidx, endidx) - - return results - # If you prefer the original argparse help output but would like narg ranges, it's possible # to enable narg ranges without the help changes using this method @@ -143,15 +143,6 @@ class TabCompleteExample(cmd2.Cmd): if not args.type: self.do_help('orig_suggest') - def complete_hybrid_suggest(self, text, line, begidx, endidx): - """ Adds tab completion to media""" - completer = argparse_completer.AutoCompleter(TabCompleteExample.suggest_parser_hybrid) - - tokens, _ = self.tokens_for_completion(line, begidx, endidx) - results = completer.complete_command(tokens, text, line, begidx, endidx) - - return results - # This variant demonstrates the AutoCompleter working with the orginial argparse. # Base argparse is unable to specify narg ranges. Autocompleter will keep expecting additional arguments # for the -d/--duration flag until you specify a new flaw or end the list it with '--' @@ -170,23 +161,98 @@ class TabCompleteExample(cmd2.Cmd): if not args.type: self.do_help('orig_suggest') - def complete_orig_suggest(self, text, line, begidx, endidx) -> List[str]: - """ Adds tab completion to media""" - completer = argparse_completer.AutoCompleter(TabCompleteExample.suggest_parser_orig) + ################################################################################### + # The media command demonstrates a completer with multiple layers of subcommands + # - This example demonstrates how to tag a completion attribute on each action, enabling argument + # completion without implementing a complete_COMMAND function - tokens, _ = self.tokens_for_completion(line, begidx, endidx) - results = completer.complete_command(tokens, text, line, begidx, endidx) + def _do_vid_media_movies(self, args) -> None: + if not args.command: + self.do_help('media movies') + elif args.command == 'list': + for movie_id in TabCompleteExample.MOVIE_DATABASE: + movie = TabCompleteExample.MOVIE_DATABASE[movie_id] + print('{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' + .format(movie['title'], movie['rating'], movie_id, + ', '.join(movie['director']), + '\n '.join(movie['actor']))) - return results + def _do_vid_media_shows(self, args) -> None: + if not args.command: + self.do_help('media shows') + + elif args.command == 'list': + for show_id in TabCompleteExample.SHOW_DATABASE: + show = TabCompleteExample.SHOW_DATABASE[show_id] + print('{}\n-----------------------------\n{} ID: {}' + .format(show['title'], show['rating'], show_id)) + for season in show['seasons']: + ep_list = show['seasons'][season] + print(' Season {}:\n {}' + .format(season, + '\n '.join(ep_list))) + print() + + video_parser = argparse_completer.ACArgumentParser(prog='media') + + video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type') + + vid_movies_parser = video_types_subparsers.add_parser('movies') + vid_movies_parser.set_defaults(func=_do_vid_media_movies) + + vid_movies_commands_subparsers = vid_movies_parser.add_subparsers(title='Commands', dest='command') + + vid_movies_list_parser = vid_movies_commands_subparsers.add_parser('list') + + vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') + vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', + choices=ratings_types) + # save a reference to the action object + director_action = vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter') + actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') + + # tag the action objects with completion providers. This can be a collection or a callable + setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors) + setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors) + + vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add') + vid_movies_add_parser.add_argument('title', help='Movie Title') + vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) + + # save a reference to the action object + director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), + required=True) + actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*') + + # tag the action objects with completion providers. This can be a collection or a callable + setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, static_list_directors) + setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, instance_query_actors) + + vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete') + + vid_shows_parser = video_types_subparsers.add_parser('shows') + vid_shows_parser.set_defaults(func=_do_vid_media_shows) + + vid_shows_commands_subparsers = vid_shows_parser.add_subparsers(title='Commands', dest='command') + + vid_shows_list_parser = vid_shows_commands_subparsers.add_parser('list') + + @with_category(CAT_AUTOCOMPLETE) + @with_argparser(video_parser) + def do_video(self, args): + """Video management command demonstrates multiple layers of subcommands being handled by AutoCompleter""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('video') ################################################################################### # The media command demonstrates a completer with multiple layers of subcommands # - This example uses a flat completion lookup dictionary - def query_actors(self) -> List[str]: - """Simulating a function that queries and returns a completion values""" - return TabCompleteExample.actors - def _do_media_movies(self, args) -> None: if not args.command: self.do_help('media movies') @@ -264,7 +330,7 @@ class TabCompleteExample(cmd2.Cmd): # name collisions. def complete_media(self, text, line, begidx, endidx): """ Adds tab completion to media""" - choices = {'actor': self.query_actors, # function + choices = {'actor': query_actors, # function 'director': TabCompleteExample.static_list_directors # static list } completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices) diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py index e68bc104..1d0c9678 100644 --- a/tests/test_autocompletion.py +++ b/tests/test_autocompletion.py @@ -213,6 +213,27 @@ def test_autocomp_subcmd_flag_comp_list(cmd2_app): assert first_match is not None and first_match == '"Gareth Edwards' +def test_autocomp_subcmd_flag_comp_func_attr(cmd2_app): + text = 'A' + line = 'video movies list -a "{}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + assert first_match is not None and \ + cmd2_app.completion_matches == ['Adam Driver', 'Alec Guinness', 'Andy Serkis', 'Anthony Daniels'] + + +def test_autocomp_subcmd_flag_comp_list_attr(cmd2_app): + text = 'G' + line = 'video movies list -d {}'.format(text) + endidx = len(line) + begidx = endidx - len(text) + + first_match = complete_tester(text, line, begidx, endidx, cmd2_app) + assert first_match is not None and first_match == '"Gareth Edwards' + + def test_autcomp_pos_consumed(cmd2_app): text = '' line = 'library movie add SW_EP01 {}'.format(text) @@ -254,3 +275,5 @@ def test_autcomp_custom_func_list_and_dict_arg(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ cmd2_app.completion_matches == ['S01E02', 'S01E03', 'S02E01', 'S02E03'] + + diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 068ea08f..27168af1 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -68,8 +68,12 @@ def test_base_argparse_help(base_app, capsys): def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'set -z') out, err = capsys.readouterr() - expected = ['usage: set [-h] [-a] [-l] [settable [settable ...]]', 'set: error: unrecognized arguments: -z'] - assert normalize(str(err)) == expected + out = normalize(out) + err = normalize(err) + assert len(err) == 3 + assert len(out) == 15 + assert 'Error: unrecognized arguments: -z' in err[0] + assert out[0] == 'usage: set [-h] [-a] [-l] [settable [settable ...]]' def test_base_shortcuts(base_app): out = run_cmd(base_app, 'shortcuts') diff --git a/tests/test_completion.py b/tests/test_completion.py index a01d1166..cf45f281 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -14,7 +14,8 @@ import sys import cmd2 import pytest -from .conftest import complete_tester +from .conftest import complete_tester, StdOut +from examples.subcommands import SubcommandsExample # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] @@ -726,76 +727,13 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ cmd2_app.display_matches == expected_display -class SubcommandsExample(cmd2.Cmd): - """ - 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) - - # subcommand functions for the base command - def base_foo(self, args): - """foo subcommand of base command""" - self.poutput(args.x * args.y) - - def base_bar(self, args): - """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 """ - 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 - base_parser = argparse.ArgumentParser(prog='base') - base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') - - # 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" 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') - - # 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): - """Base command help""" - func = getattr(args, 'func', None) - if func is not None: - # Call whatever subcommand function was selected - func(self, args) - else: - # No subcommand was provided, so call help - self.do_help('base') - - # Enable tab completion of base to make sure the subcommands' completers get called. - complete_base = cmd2.Cmd.cmd_with_subs_completer - @pytest.fixture def sc_app(): - app = SubcommandsExample() - return app + c = SubcommandsExample() + c.stdout = StdOut() + return c def test_cmd2_subcommand_completion_single_end(sc_app): text = 'f' @@ -913,12 +851,6 @@ class SubcommandsWithUnknownExample(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} - return self.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') @@ -936,10 +868,8 @@ class SubcommandsWithUnknownExample(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') - - # Set both a function and tab completer for the "sport" subcommand - parser_sport.set_defaults(func=base_sport, completer=complete_base_sport) + sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') + setattr(sport_arg, 'arg_choices', sport_item_strs) @cmd2.with_argparser_and_unknown_args(base_parser) def do_base(self, args): @@ -952,9 +882,6 @@ class SubcommandsWithUnknownExample(cmd2.Cmd): # No subcommand was provided, so call help self.do_help('base') - # Enable tab completion of base to make sure the subcommands' completers get called. - complete_base = cmd2.Cmd.cmd_with_subs_completer - @pytest.fixture def scu_app(): @@ -971,6 +898,8 @@ def test_cmd2_subcmd_with_unknown_completion_single_end(scu_app): first_match = complete_tester(text, line, begidx, endidx, scu_app) + print('first_match: {}'.format(first_match)) + # It is at end of line, so extra space is present assert first_match is not None and scu_app.completion_matches == ['foo '] |