diff options
-rwxr-xr-x | cmd2/cmd2.py | 167 | ||||
-rwxr-xr-x | examples/subcommands.py | 11 | ||||
-rw-r--r-- | tests/test_completion.py | 89 |
3 files changed, 13 insertions, 254 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 23e56f19..f9940b44 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -271,21 +271,6 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Calla cmd_wrapper.__dict__['has_parser'] = True setattr(cmd_wrapper, '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: - # 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 return arg_decorator @@ -324,22 +309,6 @@ def with_argparser(argparser: argparse.ArgumentParser) -> Callable: cmd_wrapper.__dict__['has_parser'] = True setattr(cmd_wrapper, '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: - - # 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 return arg_decorator @@ -1023,49 +992,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): @@ -1797,10 +1723,11 @@ class Cmd(cmd.Cmd): try: compfunc = getattr(self, 'complete_' + command) except AttributeError: - + # There's no completer function, next see if the command uses argparser cmd_func = getattr(self, 'do_' + command) if hasattr(cmd_func, 'has_parser') and hasattr(cmd_func, 'argparser') and \ getattr(cmd_func, 'has_parser'): + # Command uses argparser, switch to the default argparse completer argparser = getattr(cmd_func, 'argparser') compfunc = functools.partial(self._autocomplete_default, argparser=argparser) @@ -1975,6 +1902,7 @@ class Cmd(cmd.Cmd): strs_to_match = list(topics | visible_commands) matches = self.basic_complete(text, line, begidx, endidx, strs_to_match) + # check if the command uses argparser elif index >= subcmd_index and hasattr(self, 'do_' + tokens[cmd_index]) and\ hasattr(getattr(self, 'do_' + tokens[cmd_index]), 'has_parser'): command = tokens[cmd_index] @@ -1983,14 +1911,6 @@ class Cmd(cmd.Cmd): completer = AutoCompleter(parser) matches = completer.complete_command_help(tokens[1:], text, line, begidx, endidx) - - # 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)) - return matches # noinspection PyUnusedLocal @@ -2947,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/examples/subcommands.py b/examples/subcommands.py index 03088c93..8cdfb368 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -9,7 +9,7 @@ and provides separate contextual help. import argparse import cmd2 -from cmd2 import with_argparser +from cmd2 import with_argparser, with_argparser_and_unknown_args sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] @@ -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') @@ -81,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/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 '] |