Skip to content

Commit 965fa83

Browse files
committed
Switched the default behavior in cmd2 for argparse commands to use the AutoCompleter by default.
1 parent 85c2c6b commit 965fa83

3 files changed

Lines changed: 13 additions & 254 deletions

File tree

cmd2/cmd2.py

Lines changed: 3 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -271,21 +271,6 @@ def cmd_wrapper(instance, cmdline):
271271
cmd_wrapper.__dict__['has_parser'] = True
272272
setattr(cmd_wrapper, 'argparser', argparser)
273273

274-
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
275-
if argparser._subparsers is not None:
276-
# Key is subcommand name and value is completer function
277-
subcommands = collections.OrderedDict()
278-
279-
# Get all subcommands and check if they have completer functions
280-
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
281-
if 'completer' in parser._defaults:
282-
completer = parser._defaults['completer']
283-
else:
284-
completer = None
285-
subcommands[name] = completer
286-
287-
cmd_wrapper.__dict__['subcommands'] = subcommands
288-
289274
return cmd_wrapper
290275

291276
return arg_decorator
@@ -324,22 +309,6 @@ def cmd_wrapper(instance, cmdline):
324309
cmd_wrapper.__dict__['has_parser'] = True
325310
setattr(cmd_wrapper, 'argparser', argparser)
326311

327-
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
328-
if argparser._subparsers is not None:
329-
330-
# Key is subcommand name and value is completer function
331-
subcommands = collections.OrderedDict()
332-
333-
# Get all subcommands and check if they have completer functions
334-
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
335-
if 'completer' in parser._defaults:
336-
completer = parser._defaults['completer']
337-
else:
338-
completer = None
339-
subcommands[name] = completer
340-
341-
cmd_wrapper.__dict__['subcommands'] = subcommands
342-
343312
return cmd_wrapper
344313

345314
return arg_decorator
@@ -1023,49 +992,6 @@ def colorize(self, val, color):
1023992
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
1024993
return val
1025994

1026-
def get_subcommands(self, command):
1027-
"""
1028-
Returns a list of a command's subcommand names if they exist
1029-
:param command: the command we are querying
1030-
:return: A subcommand list or None
1031-
"""
1032-
1033-
subcommand_names = None
1034-
1035-
# Check if is a valid command
1036-
funcname = self._func_named(command)
1037-
1038-
if funcname:
1039-
# Check to see if this function was decorated with an argparse ArgumentParser
1040-
func = getattr(self, funcname)
1041-
subcommands = func.__dict__.get('subcommands', None)
1042-
if subcommands is not None:
1043-
subcommand_names = subcommands.keys()
1044-
1045-
return subcommand_names
1046-
1047-
def get_subcommand_completer(self, command, subcommand):
1048-
"""
1049-
Returns a subcommand's tab completion function if one exists
1050-
:param command: command which owns the subcommand
1051-
:param subcommand: the subcommand we are querying
1052-
:return: A completer or None
1053-
"""
1054-
1055-
completer = None
1056-
1057-
# Check if is a valid command
1058-
funcname = self._func_named(command)
1059-
1060-
if funcname:
1061-
# Check to see if this function was decorated with an argparse ArgumentParser
1062-
func = getattr(self, funcname)
1063-
subcommands = func.__dict__.get('subcommands', None)
1064-
if subcommands is not None:
1065-
completer = subcommands[subcommand]
1066-
1067-
return completer
1068-
1069995
# ----- Methods related to tab completion -----
1070996

1071997
def set_completion_defaults(self):
@@ -1797,10 +1723,11 @@ def complete(self, text, state):
17971723
try:
17981724
compfunc = getattr(self, 'complete_' + command)
17991725
except AttributeError:
1800-
1726+
# There's no completer function, next see if the command uses argparser
18011727
cmd_func = getattr(self, 'do_' + command)
18021728
if hasattr(cmd_func, 'has_parser') and hasattr(cmd_func, 'argparser') and \
18031729
getattr(cmd_func, 'has_parser'):
1730+
# Command uses argparser, switch to the default argparse completer
18041731
argparser = getattr(cmd_func, 'argparser')
18051732
compfunc = functools.partial(self._autocomplete_default,
18061733
argparser=argparser)
@@ -1975,6 +1902,7 @@ def complete_help(self, text, line, begidx, endidx):
19751902
strs_to_match = list(topics | visible_commands)
19761903
matches = self.basic_complete(text, line, begidx, endidx, strs_to_match)
19771904

1905+
# check if the command uses argparser
19781906
elif index >= subcmd_index and hasattr(self, 'do_' + tokens[cmd_index]) and\
19791907
hasattr(getattr(self, 'do_' + tokens[cmd_index]), 'has_parser'):
19801908
command = tokens[cmd_index]
@@ -1983,14 +1911,6 @@ def complete_help(self, text, line, begidx, endidx):
19831911
completer = AutoCompleter(parser)
19841912
matches = completer.complete_command_help(tokens[1:], text, line, begidx, endidx)
19851913

1986-
1987-
# Check if we are completing a subcommand
1988-
elif index == subcmd_index:
1989-
1990-
# Match subcommands if any exist
1991-
command = tokens[cmd_index]
1992-
matches = self.basic_complete(text, line, begidx, endidx, self.get_subcommands(command))
1993-
19941914
return matches
19951915

19961916
# noinspection PyUnusedLocal
@@ -2947,87 +2867,6 @@ def complete_shell(self, text, line, begidx, endidx):
29472867
index_dict = {1: self.shell_cmd_complete}
29482868
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
29492869

2950-
def cmd_with_subs_completer(self, text, line, begidx, endidx):
2951-
"""
2952-
This is a function provided for convenience to those who want an easy way to add
2953-
tab completion to functions that implement subcommands. By setting this as the
2954-
completer of the base command function, the correct completer for the chosen subcommand
2955-
will be called.
2956-
2957-
The use of this function requires assigning a completer function to the subcommand's parser
2958-
Example:
2959-
A command called print has a subcommands called 'names' that needs a tab completer
2960-
When you create the parser for names, include the completer function in the parser's defaults.
2961-
2962-
names_parser.set_defaults(func=print_names, completer=complete_print_names)
2963-
2964-
To make sure the names completer gets called, set the completer for the print function
2965-
in a similar fashion to what follows.
2966-
2967-
complete_print = cmd2.Cmd.cmd_with_subs_completer
2968-
2969-
When the subcommand's completer is called, this function will have stripped off all content from the
2970-
beginning of the command line before the subcommand, meaning the line parameter always starts with the
2971-
subcommand name and the index parameters reflect this change.
2972-
2973-
For instance, the command "print names -d 2" becomes "names -d 2"
2974-
begidx and endidx are incremented accordingly
2975-
2976-
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
2977-
:param line: str - the current input line with leading whitespace removed
2978-
:param begidx: int - the beginning index of the prefix text
2979-
:param endidx: int - the ending index of the prefix text
2980-
:return: List[str] - a list of possible tab completions
2981-
"""
2982-
# The command is the token at index 0 in the command line
2983-
cmd_index = 0
2984-
2985-
# The subcommand is the token at index 1 in the command line
2986-
subcmd_index = 1
2987-
2988-
# Get all tokens through the one being completed
2989-
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
2990-
if tokens is None:
2991-
return []
2992-
2993-
matches = []
2994-
2995-
# Get the index of the token being completed
2996-
index = len(tokens) - 1
2997-
2998-
# If the token being completed is past the subcommand name, then do subcommand specific tab-completion
2999-
if index > subcmd_index:
3000-
3001-
# Get the command name
3002-
command = tokens[cmd_index]
3003-
3004-
# Get the subcommand name
3005-
subcommand = tokens[subcmd_index]
3006-
3007-
# Find the offset into line where the subcommand name begins
3008-
subcmd_start = 0
3009-
for cur_index in range(0, subcmd_index + 1):
3010-
cur_token = tokens[cur_index]
3011-
subcmd_start = line.find(cur_token, subcmd_start)
3012-
3013-
if cur_index != subcmd_index:
3014-
subcmd_start += len(cur_token)
3015-
3016-
# Strip off everything before subcommand name
3017-
orig_line = line
3018-
line = line[subcmd_start:]
3019-
3020-
# Update the indexes
3021-
diff = len(orig_line) - len(line)
3022-
begidx -= diff
3023-
endidx -= diff
3024-
3025-
# Call the subcommand specific completer if it exists
3026-
compfunc = self.get_subcommand_completer(command, subcommand)
3027-
if compfunc is not None:
3028-
matches = compfunc(self, text, line, begidx, endidx)
3029-
3030-
return matches
30312870

30322871
# noinspection PyBroadException
30332872
def do_py(self, arg):

examples/subcommands.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import argparse
1010

1111
import cmd2
12-
from cmd2 import with_argparser
12+
from cmd2 import with_argparser, with_argparser_and_unknown_args
1313

1414
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
1515

@@ -35,12 +35,6 @@ def base_sport(self, args):
3535
"""sport subcommand of base command"""
3636
self.poutput('Sport is {}'.format(args.sport))
3737

38-
# noinspection PyUnusedLocal
39-
def complete_base_sport(self, text, line, begidx, endidx):
40-
""" Adds tab completion to base sport subcommand """
41-
index_dict = {1: sport_item_strs}
42-
return self.index_based_complete(text, line, begidx, endidx, index_dict)
43-
4438
# create the top-level parser for the base command
4539
base_parser = argparse.ArgumentParser(prog='base')
4640
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
@@ -81,9 +75,6 @@ def do_base(self, args):
8175
# No subcommand was provided, so call help
8276
self.do_help('base')
8377

84-
# Enable tab completion of base to make sure the subcommands' completers get called.
85-
# complete_base = cmd2.Cmd.cmd_with_subs_completer
86-
8778

8879
if __name__ == '__main__':
8980
app = SubcommandsExample()

tests/test_completion.py

Lines changed: 9 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
import cmd2
1616
import pytest
17-
from .conftest import complete_tester
17+
from .conftest import complete_tester, StdOut
18+
from examples.subcommands import SubcommandsExample
1819

1920
# List of strings used with completion functions
2021
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
@@ -726,76 +727,13 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
726727
os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \
727728
cmd2_app.display_matches == expected_display
728729

729-
class SubcommandsExample(cmd2.Cmd):
730-
"""
731-
Example cmd2 application where we a base command which has a couple subcommands
732-
and the "sport" subcommand has tab completion enabled.
733-
"""
734-
735-
def __init__(self):
736-
cmd2.Cmd.__init__(self)
737-
738-
# subcommand functions for the base command
739-
def base_foo(self, args):
740-
"""foo subcommand of base command"""
741-
self.poutput(args.x * args.y)
742-
743-
def base_bar(self, args):
744-
"""bar subcommand of base command"""
745-
self.poutput('((%s))' % args.z)
746-
747-
def base_sport(self, args):
748-
"""sport subcommand of base command"""
749-
self.poutput('Sport is {}'.format(args.sport))
750-
751-
# noinspection PyUnusedLocal
752-
def complete_base_sport(self, text, line, begidx, endidx):
753-
""" Adds tab completion to base sport subcommand """
754-
index_dict = {1: sport_item_strs}
755-
return self.index_based_complete(text, line, begidx, endidx, index_dict)
756-
757-
# create the top-level parser for the base command
758-
base_parser = argparse.ArgumentParser(prog='base')
759-
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
760-
761-
# create the parser for the "foo" subcommand
762-
parser_foo = base_subparsers.add_parser('foo', help='foo help')
763-
parser_foo.add_argument('-x', type=int, default=1, help='integer')
764-
parser_foo.add_argument('y', type=float, help='float')
765-
parser_foo.set_defaults(func=base_foo)
766-
767-
# create the parser for the "bar" subcommand
768-
parser_bar = base_subparsers.add_parser('bar', help='bar help')
769-
parser_bar.add_argument('z', help='string')
770-
parser_bar.set_defaults(func=base_bar)
771-
772-
# create the parser for the "sport" subcommand
773-
parser_sport = base_subparsers.add_parser('sport', help='sport help')
774-
parser_sport.add_argument('sport', help='Enter name of a sport')
775-
776-
# Set both a function and tab completer for the "sport" subcommand
777-
parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
778-
779-
@cmd2.with_argparser(base_parser)
780-
def do_base(self, args):
781-
"""Base command help"""
782-
func = getattr(args, 'func', None)
783-
if func is not None:
784-
# Call whatever subcommand function was selected
785-
func(self, args)
786-
else:
787-
# No subcommand was provided, so call help
788-
self.do_help('base')
789-
790-
# Enable tab completion of base to make sure the subcommands' completers get called.
791-
complete_base = cmd2.Cmd.cmd_with_subs_completer
792-
793730

794731
@pytest.fixture
795732
def sc_app():
796-
app = SubcommandsExample()
797-
return app
733+
c = SubcommandsExample()
734+
c.stdout = StdOut()
798735

736+
return c
799737

800738
def test_cmd2_subcommand_completion_single_end(sc_app):
801739
text = 'f'
@@ -913,12 +851,6 @@ def base_sport(self, args):
913851
"""sport subcommand of base command"""
914852
self.poutput('Sport is {}'.format(args.sport))
915853

916-
# noinspection PyUnusedLocal
917-
def complete_base_sport(self, text, line, begidx, endidx):
918-
""" Adds tab completion to base sport subcommand """
919-
index_dict = {1: sport_item_strs}
920-
return self.index_based_complete(text, line, begidx, endidx, index_dict)
921-
922854
# create the top-level parser for the base command
923855
base_parser = argparse.ArgumentParser(prog='base')
924856
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
@@ -936,10 +868,8 @@ def complete_base_sport(self, text, line, begidx, endidx):
936868

937869
# create the parser for the "sport" subcommand
938870
parser_sport = base_subparsers.add_parser('sport', help='sport help')
939-
parser_sport.add_argument('sport', help='Enter name of a sport')
940-
941-
# Set both a function and tab completer for the "sport" subcommand
942-
parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
871+
sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport')
872+
setattr(sport_arg, 'arg_choices', sport_item_strs)
943873

944874
@cmd2.with_argparser_and_unknown_args(base_parser)
945875
def do_base(self, args):
@@ -952,9 +882,6 @@ def do_base(self, args):
952882
# No subcommand was provided, so call help
953883
self.do_help('base')
954884

955-
# Enable tab completion of base to make sure the subcommands' completers get called.
956-
complete_base = cmd2.Cmd.cmd_with_subs_completer
957-
958885

959886
@pytest.fixture
960887
def scu_app():
@@ -971,6 +898,8 @@ def test_cmd2_subcmd_with_unknown_completion_single_end(scu_app):
971898

972899
first_match = complete_tester(text, line, begidx, endidx, scu_app)
973900

901+
print('first_match: {}'.format(first_match))
902+
974903
# It is at end of line, so extra space is present
975904
assert first_match is not None and scu_app.completion_matches == ['foo ']
976905

0 commit comments

Comments
 (0)