Skip to content

Commit c9b676a

Browse files
authored
Merge pull request #366 from python-cmd2/autocompleter
argparse Autocompleter integration into cmd2
2 parents a0a46f9 + cbb94bf commit c9b676a

File tree

9 files changed

+208
-311
lines changed

9 files changed

+208
-311
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* All ``cmd2`` code should be ported to use the new ``argparse``-based decorators
1111
* See the [Argument Processing](http://cmd2.readthedocs.io/en/latest/argument_processing.html) section of the documentation for more information on these decorators
1212
* Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py)
13+
* Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer``
14+
* Replaced by default AutoCompleter implementation for all commands using argparse
1315
* Python 2 no longer supported
1416
* ``cmd2`` now supports Python 3.4+
1517

cmd2/argparse_completer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def my_completer(text: str, line: str, begidx: int, endidx:int, extra_param: str
7171
from .rl_utils import rl_force_redisplay
7272

7373

74+
# attribute that can optionally added to an argparse argument (called an Action) to
75+
# define the completion choices for the argument. You may provide a Collection or a Function.
76+
ACTION_ARG_CHOICES = 'arg_choices'
77+
7478
class _RangeAction(object):
7579
def __init__(self, nargs: Union[int, str, Tuple[int, int], None]):
7680
self.nargs_min = None
@@ -220,6 +224,10 @@ def __init__(self,
220224
# if there are choices defined, record them in the arguments dictionary
221225
if action.choices is not None:
222226
self._arg_choices[action.dest] = action.choices
227+
# if completion choices are tagged on the action, record them
228+
elif hasattr(action, ACTION_ARG_CHOICES):
229+
action_arg_choices = getattr(action, ACTION_ARG_CHOICES)
230+
self._arg_choices[action.dest] = action_arg_choices
223231

224232
# if the parameter is flag based, it will have option_strings
225233
if action.option_strings:
@@ -406,6 +414,21 @@ def consume_positional_argument() -> None:
406414

407415
return completion_results
408416

417+
def complete_command_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
418+
"""Supports the completion of sub-commands for commands through the cmd2 help command."""
419+
for idx, token in enumerate(tokens):
420+
if idx >= self._token_start_index:
421+
if self._positional_completers:
422+
# For now argparse only allows 1 sub-command group per level
423+
# so this will only loop once.
424+
for completers in self._positional_completers.values():
425+
if token in completers:
426+
return completers[token].complete_command_help(tokens, text, line, begidx, endidx)
427+
else:
428+
return self.basic_complete(text, line, begidx, endidx, completers.keys())
429+
return []
430+
431+
409432
@staticmethod
410433
def _process_action_nargs(action: argparse.Action, arg_state: _ArgumentState) -> None:
411434
if isinstance(action, _RangeAction):
@@ -467,6 +490,7 @@ def _complete_for_arg(self, action: argparse.Action,
467490
def _resolve_choices_for_arg(self, action: argparse.Action, used_values=()) -> List[str]:
468491
if action.dest in self._arg_choices:
469492
args = self._arg_choices[action.dest]
493+
470494
if callable(args):
471495
args = args()
472496

cmd2/cmd2.py

Lines changed: 35 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
# Set up readline
5353
from .rl_utils import rl_force_redisplay, readline, rl_type, RlType
54+
from .argparse_completer import AutoCompleter, ACArgumentParser
5455

5556
if rl_type == RlType.PYREADLINE:
5657

@@ -266,23 +267,8 @@ def cmd_wrapper(instance, cmdline):
266267

267268
cmd_wrapper.__doc__ = argparser.format_help()
268269

269-
# Mark this function as having an argparse ArgumentParser (used by do_help)
270-
cmd_wrapper.__dict__['has_parser'] = True
271-
272-
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
273-
if argparser._subparsers is not None:
274-
# Key is subcommand name and value is completer function
275-
subcommands = collections.OrderedDict()
276-
277-
# Get all subcommands and check if they have completer functions
278-
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
279-
if 'completer' in parser._defaults:
280-
completer = parser._defaults['completer']
281-
else:
282-
completer = None
283-
subcommands[name] = completer
284-
285-
cmd_wrapper.__dict__['subcommands'] = subcommands
270+
# Mark this function as having an argparse ArgumentParser
271+
setattr(cmd_wrapper, 'argparser', argparser)
286272

287273
return cmd_wrapper
288274

@@ -318,24 +304,8 @@ def cmd_wrapper(instance, cmdline):
318304

319305
cmd_wrapper.__doc__ = argparser.format_help()
320306

321-
# Mark this function as having an argparse ArgumentParser (used by do_help)
322-
cmd_wrapper.__dict__['has_parser'] = True
323-
324-
# If there are subcommands, store their names in a list to support tab-completion of subcommand names
325-
if argparser._subparsers is not None:
326-
327-
# Key is subcommand name and value is completer function
328-
subcommands = collections.OrderedDict()
329-
330-
# Get all subcommands and check if they have completer functions
331-
for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items():
332-
if 'completer' in parser._defaults:
333-
completer = parser._defaults['completer']
334-
else:
335-
completer = None
336-
subcommands[name] = completer
337-
338-
cmd_wrapper.__dict__['subcommands'] = subcommands
307+
# Mark this function as having an argparse ArgumentParser
308+
setattr(cmd_wrapper, 'argparser', argparser)
339309

340310
return cmd_wrapper
341311

@@ -1020,49 +990,6 @@ def colorize(self, val, color):
1020990
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
1021991
return val
1022992

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

1068995
def set_completion_defaults(self):
@@ -1794,16 +1721,14 @@ def complete(self, text, state):
17941721
try:
17951722
compfunc = getattr(self, 'complete_' + command)
17961723
except AttributeError:
1797-
compfunc = self.completedefault
1798-
1799-
subcommands = self.get_subcommands(command)
1800-
if subcommands is not None:
1801-
# Since there are subcommands, then try completing those if the cursor is in
1802-
# the token at index 1, otherwise default to using compfunc
1803-
index_dict = {1: subcommands}
1804-
compfunc = functools.partial(self.index_based_complete,
1805-
index_dict=index_dict,
1806-
all_else=compfunc)
1724+
# There's no completer function, next see if the command uses argparser
1725+
try:
1726+
cmd_func = getattr(self, 'do_' + command)
1727+
argparser = getattr(cmd_func, 'argparser')
1728+
# Command uses argparser, switch to the default argparse completer
1729+
compfunc = functools.partial(self._autocomplete_default, argparser=argparser)
1730+
except AttributeError:
1731+
compfunc = self.completedefault
18071732

18081733
# A valid command was not entered
18091734
else:
@@ -1910,6 +1835,16 @@ def complete(self, text, state):
19101835
except IndexError:
19111836
return None
19121837

1838+
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
1839+
argparser: argparse.ArgumentParser) -> List[str]:
1840+
"""Default completion function for argparse commands."""
1841+
completer = AutoCompleter(argparser)
1842+
1843+
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
1844+
results = completer.complete_command(tokens, text, line, begidx, endidx)
1845+
1846+
return results
1847+
19131848
def get_all_commands(self):
19141849
"""
19151850
Returns a list of all commands
@@ -1964,12 +1899,15 @@ def complete_help(self, text, line, begidx, endidx):
19641899
strs_to_match = list(topics | visible_commands)
19651900
matches = self.basic_complete(text, line, begidx, endidx, strs_to_match)
19661901

1967-
# Check if we are completing a subcommand
1968-
elif index == subcmd_index:
1969-
1970-
# Match subcommands if any exist
1971-
command = tokens[cmd_index]
1972-
matches = self.basic_complete(text, line, begidx, endidx, self.get_subcommands(command))
1902+
# check if the command uses argparser
1903+
elif index >= subcmd_index:
1904+
try:
1905+
cmd_func = getattr(self, 'do_' + tokens[cmd_index])
1906+
parser = getattr(cmd_func, 'argparser')
1907+
completer = AutoCompleter(parser)
1908+
matches = completer.complete_command_help(tokens[1:], text, line, begidx, endidx)
1909+
except AttributeError:
1910+
pass
19731911

19741912
return matches
19751913

@@ -2620,7 +2558,7 @@ def do_help(self, arglist):
26202558
if funcname:
26212559
# Check to see if this function was decorated with an argparse ArgumentParser
26222560
func = getattr(self, funcname)
2623-
if func.__dict__.get('has_parser', False):
2561+
if hasattr(func, 'argparser'):
26242562
# Function has an argparser, so get help based on all the arguments in case there are sub-commands
26252563
new_arglist = arglist[1:]
26262564
new_arglist.append('-h')
@@ -2843,10 +2781,10 @@ def show(self, args, parameter):
28432781
else:
28442782
raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param)
28452783

2846-
set_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
2784+
set_parser = ACArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
28472785
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
28482786
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
2849-
set_parser.add_argument('settable', nargs='*', help='[param_name] [value]')
2787+
set_parser.add_argument('settable', nargs=(0,2), help='[param_name] [value]')
28502788

28512789
@with_argparser(set_parser)
28522790
def do_set(self, args):
@@ -2927,87 +2865,6 @@ def complete_shell(self, text, line, begidx, endidx):
29272865
index_dict = {1: self.shell_cmd_complete}
29282866
return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete)
29292867

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

30122869
# noinspection PyBroadException
30132870
def do_py(self, arg):

docs/argument_processing.rst

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,10 @@ Sub-commands
346346
Sub-commands are supported for commands using either the ``@with_argparser`` or
347347
``@with_argparser_and_unknown_args`` decorator. The syntax for supporting them is based on argparse sub-parsers.
348348

349-
Also, a convenience function called ``cmd_with_subs_completer`` is available to easily add tab completion to functions
350-
that implement subcommands. By setting this as the completer of the base command function, the correct completer for
351-
the chosen subcommand will be called.
349+
You may add multiple layers of sub-commands for your command. Cmd2 will automatically traverse and tab-complete
350+
sub-commands for all commands using argparse.
352351

353-
See the subcommands_ example to learn more about how to use sub-commands in your ``cmd2`` application.
354-
This example also demonstrates usage of ``cmd_with_subs_completer``. In addition, the docstring for
355-
``cmd_with_subs_completer`` offers more details.
352+
See the subcommands_ and tab_autocompletion_ example to learn more about how to use sub-commands in your ``cmd2`` application.
356353

357354
.. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py
355+
.. _tab_autocompletion: https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py

0 commit comments

Comments
 (0)