summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xcmd2/argparse_completer.py44
-rwxr-xr-xcmd2/cmd2.py215
-rw-r--r--docs/argument_processing.rst10
-rwxr-xr-xexamples/subcommands.py22
-rwxr-xr-xexamples/tab_autocompletion.py132
-rw-r--r--tests/test_autocompletion.py23
-rw-r--r--tests/test_cmd2.py8
-rw-r--r--tests/test_completion.py89
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 ']