Skip to content

Commit d78dc46

Browse files
committed
Simplified how to add tab completion to a subcommand
1 parent 554561b commit d78dc46

4 files changed

Lines changed: 91 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
* ``exclude_from_help`` is now called ``hidden_commands`` since these commands are hidden from things other than help, including tab completion
1616
* This list also no longer takes the function names of commands (``do_history``), but instead uses the command names themselves (``history``)
1717
* ``excludeFromHistory`` is now called ``exclude_from_history``
18+
* ``cmd_with_subs_completer()`` no longer takes an argument called ``base``. Adding tab completion to subcommands has been simplified to declaring it in the
19+
subcommand parser's default settings. This easily allows arbitrary completers like path_complete to be used.
20+
See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use
21+
tab completion in subcommands. In addition, the docstring for ``cmd_with_subs_completer()`` offers more details.
1822

1923

2024
## 0.8.2 (March 21, 2018)
@@ -75,7 +79,7 @@
7579
* See the [Argument Processing](http://cmd2.readthedocs.io/en/latest/argument_processing.html) section of the documentation for more information on these decorators
7680
* Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py)
7781
and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples
78-
* Added support for Argpasre sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators
82+
* Added support for Argparse sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators
7983
* See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands
8084
* Tab-completion of sub-command names is automatically supported
8185
* The **__relative_load** command is now hidden from the help menu by default

cmd2.py

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,19 @@ def cmd_wrapper(instance, cmdline):
430430

431431
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
432432
if argparser._subparsers is not None:
433-
subcommand_names = argparser._subparsers._group_actions[0]._name_parser_map.keys()
434-
cmd_wrapper.__dict__['subcommand_names'] = subcommand_names
433+
434+
# Key is subcommand name and value is completer function
435+
subcommands = collections.OrderedDict()
436+
437+
# Get all subcommands and check if they have completer functions
438+
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
439+
if 'completer' in parser._defaults:
440+
completer = parser._defaults['completer']
441+
else:
442+
completer = None
443+
subcommands[name] = completer
444+
445+
cmd_wrapper.__dict__['subcommands'] = subcommands
435446

436447
return cmd_wrapper
437448

@@ -1214,8 +1225,8 @@ def colorize(self, val, color):
12141225

12151226
def get_subcommands(self, command):
12161227
"""
1217-
Returns a list of a command's subcommands if they exist
1218-
:param command:
1228+
Returns a list of a command's subcommand names if they exist
1229+
:param command: the command we are querying
12191230
:return: A subcommand list or None
12201231
"""
12211232

@@ -1227,10 +1238,34 @@ def get_subcommands(self, command):
12271238
if funcname:
12281239
# Check to see if this function was decorated with an argparse ArgumentParser
12291240
func = getattr(self, funcname)
1230-
subcommand_names = func.__dict__.get('subcommand_names', None)
1241+
subcommands = func.__dict__.get('subcommands', None)
1242+
if subcommands is not None:
1243+
subcommand_names = subcommands.keys()
12311244

12321245
return subcommand_names
12331246

1247+
def get_subcommand_completer(self, command, subcommand):
1248+
"""
1249+
Returns a subcommand's tab completion function if one exists
1250+
:param command: command which owns the subcommand
1251+
:param subcommand: the subcommand we are querying
1252+
:return: A completer or None
1253+
"""
1254+
1255+
completer = None
1256+
1257+
# Check if is a valid command
1258+
funcname = self._func_named(command)
1259+
1260+
if funcname:
1261+
# Check to see if this function was decorated with an argparse ArgumentParser
1262+
func = getattr(self, funcname)
1263+
subcommands = func.__dict__.get('subcommands', None)
1264+
if subcommands is not None:
1265+
completer = subcommands[subcommand]
1266+
1267+
return completer
1268+
12341269
# ----- Methods related to tab completion -----
12351270

12361271
def set_completion_defaults(self):
@@ -3069,40 +3104,40 @@ def complete_shell(self, text, line, begidx, endidx):
30693104
index_dict = {1: self.shell_cmd_complete}
30703105
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
30713106

3072-
def cmd_with_subs_completer(self, text, line, begidx, endidx, base):
3107+
def cmd_with_subs_completer(self, text, line, begidx, endidx):
30733108
"""
30743109
This is a function provided for convenience to those who want an easy way to add
30753110
tab completion to functions that implement subcommands. By setting this as the
30763111
completer of the base command function, the correct completer for the chosen subcommand
30773112
will be called.
30783113
3079-
The use of this function requires a particular naming scheme.
3114+
The use of this function requires assigning a completer function to the subcommand's parser
30803115
Example:
3081-
A command called print has 2 subcommands [names, addresses]
3082-
The tab-completion functions for the subcommands must be called:
3083-
names -> complete_print_names
3084-
addresses -> complete_print_addresses
3116+
A command called print has a subcommands called 'names' that needs a tab completer
3117+
When you create the parser for names, include the completer function in the parser's defaults.
3118+
3119+
names_parser.set_defaults(func=print_names, completer=complete_print_names)
30853120
3086-
To make sure these functions get called, set the tab-completer for the print function
3087-
in a similar fashion to what follows where base is the name of the root command (print)
3121+
To make sure the names completer gets called, set the completer for the print function
3122+
in a similar fashion to what follows.
30883123
3089-
def complete_print(self, text, line, begidx, endidx):
3090-
return self.cmd_with_subs_completer(text, line, begidx, endidx, base='print')
3124+
complete_print = cmd2.Cmd.cmd_with_subs_completer
30913125
3092-
When the subcommand's completer is called, this function will have stripped off all content from the
3093-
beginning of the command line before the subcommand, meaning the line parameter always starts with the
3094-
subcommand name and the index parameters reflect this change.
3126+
When the subcommand's completer is called, this function will have stripped off all content from the
3127+
beginning of the command line before the subcommand, meaning the line parameter always starts with the
3128+
subcommand name and the index parameters reflect this change.
30953129
3096-
For instance, the command "print names -d 2" becomes "names -d 2"
3097-
begidx and endidx are incremented accordingly
3130+
For instance, the command "print names -d 2" becomes "names -d 2"
3131+
begidx and endidx are incremented accordingly
30983132
30993133
:param text: str - the string prefix we are attempting to match (all returned matches must begin with it)
31003134
:param line: str - the current input line with leading whitespace removed
31013135
:param begidx: int - the beginning index of the prefix text
31023136
:param endidx: int - the ending index of the prefix text
3103-
:param base: str - the name of the base command that owns the subcommands
31043137
:return: List[str] - a sorted list of possible tab completions
31053138
"""
3139+
# The command is the token at index 0 in the command line
3140+
cmd_index = 0
31063141

31073142
# The subcommand is the token at index 1 in the command line
31083143
subcmd_index = 1
@@ -3120,6 +3155,9 @@ def complete_print(self, text, line, begidx, endidx):
31203155
# If the token being completed is past the subcommand name, then do subcommand specific tab-completion
31213156
if index > subcmd_index:
31223157

3158+
# Get the command name
3159+
command = tokens[cmd_index]
3160+
31233161
# Get the subcommand name
31243162
subcommand = tokens[subcmd_index]
31253163

@@ -3142,11 +3180,9 @@ def complete_print(self, text, line, begidx, endidx):
31423180
endidx -= diff
31433181

31443182
# Call the subcommand specific completer if it exists
3145-
completer = 'complete_{}_{}'.format(base, subcommand)
3146-
compfunc = getattr(self, completer, None)
3147-
3183+
compfunc = self.get_subcommand_completer(command, subcommand)
31483184
if compfunc is not None:
3149-
matches = compfunc(text, line, begidx, endidx)
3185+
matches = compfunc(self, text, line, begidx, endidx)
31503186

31513187
return matches
31523188

examples/subcommands.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
import cmd2
1212
from cmd2 import with_argparser
1313

14+
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
1415

1516
class SubcommandsExample(cmd2.Cmd):
16-
""" Example cmd2 application where we a base command which has a couple subcommands."""
17+
"""
18+
Example cmd2 application where we a base command which has a couple subcommands
19+
and the "sport" subcommand has tab completion enabled.
20+
"""
1721

1822
def __init__(self):
1923
cmd2.Cmd.__init__(self)
@@ -34,8 +38,7 @@ def base_sport(self, args):
3438
# noinspection PyUnusedLocal
3539
def complete_base_sport(self, text, line, begidx, endidx):
3640
""" Adds tab completion to base sport subcommand """
37-
sports = ['Football', 'Hockey', 'Soccer', 'Baseball']
38-
index_dict = {1: sports}
41+
index_dict = {1: sport_item_strs}
3942
return self.index_based_complete(text, line, begidx, endidx, index_dict)
4043

4144
# create the top-level parser for the base command
@@ -56,7 +59,9 @@ def complete_base_sport(self, text, line, begidx, endidx):
5659
# create the parser for the "sport" subcommand
5760
parser_sport = base_subparsers.add_parser('sport', help='sport help')
5861
parser_sport.add_argument('sport', help='Enter name of a sport')
59-
parser_sport.set_defaults(func=base_sport)
62+
63+
# Set both a function and tab completer for the "sport" subcommand
64+
parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
6065

6166
@with_argparser(base_parser)
6267
def do_base(self, args):
@@ -69,8 +74,8 @@ def do_base(self, args):
6974
# No subcommand was provided, so call help
7075
self.do_help('base')
7176

72-
def complete_base(self, text, line, begidx, endidx):
73-
return self.cmd_with_subs_completer(text, line, begidx, endidx, base='base')
77+
# Enable tab completion of base to make sure the subcommands' completers get called.
78+
complete_base = cmd2.Cmd.cmd_with_subs_completer
7479

7580

7681
if __name__ == '__main__':

tests/test_completion.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -631,12 +631,15 @@ def test_parseline_expands_shortcuts(cmd2_app):
631631

632632

633633
class SubcommandsExample(cmd2.Cmd):
634-
""" Example cmd2 application where we a base command which has a couple subcommands."""
634+
"""
635+
Example cmd2 application where we a base command which has a couple subcommands
636+
and the "sport" subcommand has tab completion enabled.
637+
"""
635638

636639
def __init__(self):
637640
cmd2.Cmd.__init__(self)
638641

639-
# sub-command functions for the base command
642+
# subcommand functions for the base command
640643
def base_foo(self, args):
641644
"""foo subcommand of base command"""
642645
self.poutput(args.x * args.y)
@@ -649,6 +652,7 @@ def base_sport(self, args):
649652
"""sport subcommand of base command"""
650653
self.poutput('Sport is {}'.format(args.sport))
651654

655+
# noinspection PyUnusedLocal
652656
def complete_base_sport(self, text, line, begidx, endidx):
653657
""" Adds tab completion to base sport subcommand """
654658
index_dict = {1: sport_item_strs}
@@ -658,21 +662,23 @@ def complete_base_sport(self, text, line, begidx, endidx):
658662
base_parser = argparse.ArgumentParser(prog='base')
659663
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
660664

661-
# create the parser for the "foo" sub-command
665+
# create the parser for the "foo" subcommand
662666
parser_foo = base_subparsers.add_parser('foo', help='foo help')
663667
parser_foo.add_argument('-x', type=int, default=1, help='integer')
664668
parser_foo.add_argument('y', type=float, help='float')
665669
parser_foo.set_defaults(func=base_foo)
666670

667-
# create the parser for the "bar" sub-command
671+
# create the parser for the "bar" subcommand
668672
parser_bar = base_subparsers.add_parser('bar', help='bar help')
669673
parser_bar.add_argument('z', help='string')
670674
parser_bar.set_defaults(func=base_bar)
671675

672676
# create the parser for the "sport" subcommand
673677
parser_sport = base_subparsers.add_parser('sport', help='sport help')
674678
parser_sport.add_argument('sport', help='Enter name of a sport')
675-
parser_sport.set_defaults(func=base_sport)
679+
680+
# Set both a function and tab completer for the "sport" subcommand
681+
parser_sport.set_defaults(func=base_sport, completer=complete_base_sport)
676682

677683
@cmd2.with_argparser(base_parser)
678684
def do_base(self, args):
@@ -682,11 +688,11 @@ def do_base(self, args):
682688
# Call whatever subcommand function was selected
683689
func(self, args)
684690
else:
685-
# No sub-command was provided, so as called
691+
# No subcommand was provided, so call help
686692
self.do_help('base')
687693

688-
def complete_base(self, text, line, begidx, endidx):
689-
return self.cmd_with_subs_completer(text, line, begidx, endidx, base='base')
694+
# Enable tab completion of base to make sure the subcommands' completers get called.
695+
complete_base = cmd2.Cmd.cmd_with_subs_completer
690696

691697

692698
@pytest.fixture

0 commit comments

Comments
 (0)