summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--cmd2/__init__.py4
-rw-r--r--cmd2/argparse_completer.py257
-rw-r--r--cmd2/argparse_custom.py38
-rw-r--r--cmd2/cmd2.py36
-rw-r--r--cmd2/utils.py29
-rw-r--r--examples/argparse_completion.py4
-rwxr-xr-xexamples/basic_completion.py28
-rw-r--r--tests/test_argparse_completer.py38
9 files changed, 223 insertions, 215 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb328f11..cb81ab17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,12 @@
* `__name__`: __main__
* `__file__`: script path (as typed, ~ will be expanded)
* Only tab complete after redirection tokens if redirection is allowed
+ * Made `CompletionError` exception available to non-argparse tab completion
+ * Added `apply_style` to `CompletionError` initializer. It defaults to True, but can be set to False if
+ you don't want the error text to have `ansi.style_error()` applied to it when printed.
* Other
* Removed undocumented `py run` command since it was replaced by `run_pyscript` a while ago
+ * Renamed `AutoCompleter` to `ArgparseCompleter` for clarity
## 0.10.0 (February 7, 2020)
* Enhancements
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 43578e46..73d70821 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -11,7 +11,7 @@ except DistributionNotFound:
pass
from .ansi import style, fg, bg
-from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser
+from .argparse_custom import Cmd2ArgumentParser, CompletionItem, set_default_argument_parser
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
import argparse
@@ -27,4 +27,4 @@ from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
from .parsing import Statement
from .py_bridge import CommandResult
-from .utils import Settable
+from .utils import CompletionError, Settable
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 185e01a2..707b36ba 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -2,7 +2,7 @@
# flake8: noqa C901
# NOTE: Ignoring flake8 cyclomatic complexity in this file
"""
-This module defines the AutoCompleter class which provides argparse-based tab completion to cmd2 apps.
+This module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps.
See the header of argparse_custom.py for instructions on how to use these features.
"""
@@ -10,17 +10,15 @@ import argparse
import inspect
import numbers
import shutil
-import textwrap
from collections import deque
from typing import Dict, List, Optional, Union
from . import ansi
from . import cmd2
-from . import utils
from .argparse_custom import ATTR_CHOICES_CALLABLE, INFINITY, generate_range_error
from .argparse_custom import ATTR_SUPPRESS_TAB_HINT, ATTR_DESCRIPTIVE_COMPLETION_HEADER, ATTR_NARGS_RANGE
-from .argparse_custom import ChoicesCallable, CompletionError, CompletionItem
-from .rl_utils import rl_force_redisplay
+from .argparse_custom import ChoicesCallable, CompletionItem
+from .utils import basic_complete, CompletionError
# If no descriptive header is supplied, then this will be used instead
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
@@ -63,52 +61,89 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
return True
+class _ArgumentState:
+ """Keeps state of an argument being parsed"""
+ def __init__(self, arg_action: argparse.Action) -> None:
+ self.action = arg_action
+ self.min = None
+ self.max = None
+ self.count = 0
+ self.is_remainder = (self.action.nargs == argparse.REMAINDER)
+
+ # Check if nargs is a range
+ nargs_range = getattr(self.action, ATTR_NARGS_RANGE, None)
+ if nargs_range is not None:
+ self.min = nargs_range[0]
+ self.max = nargs_range[1]
+
+ # Otherwise check against argparse types
+ elif self.action.nargs is None:
+ self.min = 1
+ self.max = 1
+ elif self.action.nargs == argparse.OPTIONAL:
+ self.min = 0
+ self.max = 1
+ elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER:
+ self.min = 0
+ self.max = INFINITY
+ elif self.action.nargs == argparse.ONE_OR_MORE:
+ self.min = 1
+ self.max = INFINITY
+ else:
+ self.min = self.action.nargs
+ self.max = self.action.nargs
+
+
# noinspection PyProtectedMember
-class AutoCompleter:
- """Automatic command line tab completion based on argparse parameters"""
+class _UnfinishedFlagError(CompletionError):
+ def __init__(self, flag_arg_state: _ArgumentState) -> None:
+ """
+ CompletionError which occurs when the user has not finished the current flag
+ :param flag_arg_state: information about the unfinished flag action
+ """
+ error = "Error: argument {}: {} ({} entered)".\
+ format(argparse._get_action_name(flag_arg_state.action),
+ generate_range_error(flag_arg_state.min, flag_arg_state.max),
+ flag_arg_state.count)
+ super().__init__(error)
- class _ArgumentState:
- """Keeps state of an argument being parsed"""
-
- def __init__(self, arg_action: argparse.Action) -> None:
- self.action = arg_action
- self.min = None
- self.max = None
- self.count = 0
- self.is_remainder = (self.action.nargs == argparse.REMAINDER)
-
- # Check if nargs is a range
- nargs_range = getattr(self.action, ATTR_NARGS_RANGE, None)
- if nargs_range is not None:
- self.min = nargs_range[0]
- self.max = nargs_range[1]
-
- # Otherwise check against argparse types
- elif self.action.nargs is None:
- self.min = 1
- self.max = 1
- elif self.action.nargs == argparse.OPTIONAL:
- self.min = 0
- self.max = 1
- elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER:
- self.min = 0
- self.max = INFINITY
- elif self.action.nargs == argparse.ONE_OR_MORE:
- self.min = 1
- self.max = INFINITY
- else:
- self.min = self.action.nargs
- self.max = self.action.nargs
+# noinspection PyProtectedMember
+class _NoResultsError(CompletionError):
+ def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
+ """
+ CompletionError which occurs when there are no results. If hinting is allowed, then its message will
+ be a hint about the argument being tab completed.
+ :param parser: ArgumentParser instance which owns the action being tab completed
+ :param arg_action: action being tab completed
+ """
+ # Check if hinting is disabled
+ suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False)
+ if suppress_hint or arg_action.help == argparse.SUPPRESS:
+ hint_str = ''
+ else:
+ # Use the parser's help formatter to print just this action's help text
+ formatter = parser._get_formatter()
+ formatter.start_section("Hint")
+ formatter.add_argument(arg_action)
+ formatter.end_section()
+ hint_str = formatter.format_help()
+ # Set apply_style to False because we don't want hints to look like errors
+ super().__init__(hint_str, apply_style=False)
+
+
+# noinspection PyProtectedMember
+class ArgparseCompleter:
+ """Automatic command line tab completion based on argparse parameters"""
def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
parent_tokens: Optional[Dict[str, List[str]]] = None) -> None:
"""
- Create an AutoCompleter
+ Create an ArgparseCompleter
:param parser: ArgumentParser instance
- :param cmd2_app: reference to the Cmd2 application that owns this AutoCompleter
+ :param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter
:param parent_tokens: optional dictionary mapping parent parsers' arg names to their tokens
- this is only used by AutoCompleter when recursing on subcommand parsers
+ This is only used by ArgparseCompleter when recursing on subcommand parsers
Defaults to None
"""
self._parser = parser
@@ -141,7 +176,10 @@ class AutoCompleter:
self._subcommand_action = action
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
- """Complete the command using the argparse metadata and provided argument dictionary"""
+ """
+ Complete the command using the argparse metadata and provided argument dictionary
+ :raises: CompletionError for various types of tab completion errors
+ """
if not tokens:
return []
@@ -167,18 +205,18 @@ class AutoCompleter:
# Completed mutually exclusive groups
completed_mutex_groups = dict() # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
- def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
+ def consume_argument(arg_state: _ArgumentState) -> None:
"""Consuming token as an argument"""
arg_state.count += 1
consumed_arg_values.setdefault(arg_state.action.dest, [])
consumed_arg_values[arg_state.action.dest].append(token)
- def update_mutex_groups(arg_action: argparse.Action) -> bool:
+ def update_mutex_groups(arg_action: argparse.Action) -> None:
"""
Check if an argument belongs to a mutually exclusive group and either mark that group
as complete or print an error if the group has already been completed
:param arg_action: the action of the argument
- :return: False if the group has already been completed and there is a conflict, otherwise True
+ :raises: CompletionError if the group is already completed
"""
# Check if this action is in a mutually exclusive group
for group in self._parser._mutually_exclusive_groups:
@@ -191,13 +229,12 @@ class AutoCompleter:
# since it's allowed to appear on the command line more than once.
completer_action = completed_mutex_groups[group]
if arg_action == completer_action:
- return True
+ return
- error = ansi.style_error("\nError: argument {}: not allowed with argument {}\n".
- format(argparse._get_action_name(arg_action),
- argparse._get_action_name(completer_action)))
- self._print_message(error)
- return False
+ error = ("Error: argument {}: not allowed with argument {}".
+ format(argparse._get_action_name(arg_action),
+ argparse._get_action_name(completer_action)))
+ raise CompletionError(error)
# Mark that this action completed the group
completed_mutex_groups[group] = arg_action
@@ -214,8 +251,6 @@ class AutoCompleter:
# Arg can only be in one group, so we are done
break
- return True
-
#############################################################################################
# Parse all but the last token
#############################################################################################
@@ -238,8 +273,7 @@ class AutoCompleter:
elif token == '--' and not skip_remaining_flags:
# Check if there is an unfinished flag
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
- self._print_unfinished_flag_error(flag_arg_state)
- return []
+ raise _UnfinishedFlagError(flag_arg_state)
# Otherwise end the current flag
else:
@@ -252,8 +286,7 @@ class AutoCompleter:
# Check if there is an unfinished flag
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
- self._print_unfinished_flag_error(flag_arg_state)
- return []
+ raise _UnfinishedFlagError(flag_arg_state)
# Reset flag arg state but not positional tracking because flags can be
# interspersed anywhere between positionals
@@ -269,9 +302,7 @@ class AutoCompleter:
action = self._flag_to_action[candidates_flags[0]]
if action is not None:
- if not update_mutex_groups(action):
- return []
-
+ update_mutex_groups(action)
if isinstance(action, (argparse._AppendAction,
argparse._AppendConstAction,
argparse._CountAction)):
@@ -286,7 +317,7 @@ class AutoCompleter:
# earlier in the command line. Reset them now for this use of it.
consumed_arg_values[action.dest] = []
- new_arg_state = AutoCompleter._ArgumentState(action)
+ new_arg_state = _ArgumentState(action)
# Keep track of this flag if it can receive arguments
if new_arg_state.max > 0:
@@ -319,8 +350,8 @@ class AutoCompleter:
if action.dest != argparse.SUPPRESS:
parent_tokens[action.dest] = [token]
- completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app,
- parent_tokens=parent_tokens)
+ completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app,
+ parent_tokens=parent_tokens)
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx)
else:
# Invalid subcommand entered, so no way to complete remaining tokens
@@ -328,14 +359,11 @@ class AutoCompleter:
# Otherwise keep track of the argument
else:
- pos_arg_state = AutoCompleter._ArgumentState(action)
+ pos_arg_state = _ArgumentState(action)
# Check if we have a positional to consume this token
if pos_arg_state is not None:
- # No need to check for an error since we remove a completed group's positional from
- # remaining_positionals which means this action can't belong to a completed mutex group
update_mutex_groups(pos_arg_state.action)
-
consume_argument(pos_arg_state)
# No more flags are allowed if this is a REMAINDER argument
@@ -361,21 +389,15 @@ class AutoCompleter:
# character (-f) at the end.
if _looks_like_flag(text, self._parser) and not skip_remaining_flags:
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
- self._print_unfinished_flag_error(flag_arg_state)
- return []
-
+ raise _UnfinishedFlagError(flag_arg_state)
return self._complete_flags(text, line, begidx, endidx, matched_flags)
completion_results = []
# Check if we are completing a flag's argument
if flag_arg_state is not None:
- try:
- completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
- begidx, endidx, consumed_arg_values)
- except CompletionError as ex:
- self._print_completion_error(flag_arg_state.action, ex)
- return []
+ completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
+ begidx, endidx, consumed_arg_values)
# If we have results, then return them
if completion_results:
@@ -384,8 +406,7 @@ class AutoCompleter:
# Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag
elif flag_arg_state.count < flag_arg_state.min or \
not _single_prefix_char(text, self._parser) or skip_remaining_flags:
- self._print_arg_hint(flag_arg_state.action)
- return []
+ raise _NoResultsError(self._parser, flag_arg_state.action)
# Otherwise check if we have a positional to complete
elif pos_arg_state is not None or remaining_positionals:
@@ -393,14 +414,10 @@ class AutoCompleter:
# If we aren't current tracking a positional, then get the next positional arg to handle this token
if pos_arg_state is None:
action = remaining_positionals.popleft()
- pos_arg_state = AutoCompleter._ArgumentState(action)
+ pos_arg_state = _ArgumentState(action)
- try:
- completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
- begidx, endidx, consumed_arg_values)
- except CompletionError as ex:
- self._print_completion_error(pos_arg_state.action, ex)
- return []
+ completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
+ begidx, endidx, consumed_arg_values)
# If we have results, then return them
if completion_results:
@@ -408,8 +425,7 @@ class AutoCompleter:
# Otherwise, print a hint if text isn't possibly the start of a flag
elif not _single_prefix_char(text, self._parser) or skip_remaining_flags:
- self._print_arg_hint(pos_arg_state.action)
- return []
+ raise _NoResultsError(self._parser, pos_arg_state.action)
# Handle case in which text is a single flag prefix character that
# didn't complete against any argument values.
@@ -432,7 +448,7 @@ class AutoCompleter:
if action.help != argparse.SUPPRESS:
match_against.append(flag)
- return utils.basic_complete(text, line, begidx, endidx, match_against)
+ return basic_complete(text, line, begidx, endidx, match_against)
def _format_completions(self, action, completions: List[Union[str, CompletionItem]]) -> List[str]:
# Check if the results are CompletionItems and that there aren't too many to display
@@ -483,15 +499,15 @@ class AutoCompleter:
:return: List of subcommand completions
"""
# If our parser has subcommands, we must examine the tokens and check if they are subcommands
- # If so, we will let the subcommand's parser handle the rest of the tokens via another AutoCompleter.
+ # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
if self._subcommand_action is not None:
for token_index, token in enumerate(tokens[1:], start=1):
if token in self._subcommand_action.choices:
- completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app)
+ completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
return completer.complete_subcommand_help(tokens[token_index:], text, line, begidx, endidx)
elif token_index == len(tokens) - 1:
# Since this is the last token, we will attempt to complete it
- return utils.basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
+ return basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
else:
break
return []
@@ -503,11 +519,11 @@ class AutoCompleter:
:return: help text of the command being queried
"""
# If our parser has subcommands, we must examine the tokens and check if they are subcommands
- # If so, we will let the subcommand's parser handle the rest of the tokens via another AutoCompleter.
+ # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
if self._subcommand_action is not None:
for token_index, token in enumerate(tokens[1:], start=1):
if token in self._subcommand_action.choices:
- completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app)
+ completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
return completer.format_help(tokens[token_index:])
else:
break
@@ -519,7 +535,7 @@ class AutoCompleter:
"""
Tab completion routine for an argparse argument
:return: list of completions
- :raises CompletionError if the completer or choices function this calls raises one
+ :raises: CompletionError if the completer or choices function this calls raises one
"""
# Check if the arg provides choices to the user
if arg_action.choices is not None:
@@ -579,55 +595,6 @@ class AutoCompleter:
arg_choices = [choice for choice in arg_choices if choice not in used_values]
# Do tab completion on the choices
- results = utils.basic_complete(text, line, begidx, endidx, arg_choices)
+ results = basic_complete(text, line, begidx, endidx, arg_choices)
return self._format_completions(arg_action, results)
-
- @staticmethod
- def _print_message(msg: str) -> None:
- """Print a message instead of tab completions and redraw the prompt and input line"""
- import sys
- ansi.style_aware_write(sys.stdout, msg + '\n')
- rl_force_redisplay()
-
- def _print_arg_hint(self, arg_action: argparse.Action) -> None:
- """
- Print argument hint to the terminal when tab completion results in no results
- :param arg_action: action being tab completed
- """
- # Check if hinting is disabled
- suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False)
- if suppress_hint or arg_action.help == argparse.SUPPRESS:
- return
-
- # Use the parser's help formatter to print just this action's help text
- formatter = self._parser._get_formatter()
- formatter.start_section("Hint")
- formatter.add_argument(arg_action)
- formatter.end_section()
- out_str = formatter.format_help()
- self._print_message('\n' + out_str)
-
- def _print_unfinished_flag_error(self, flag_arg_state: _ArgumentState) -> None:
- """
- Print an error during tab completion when the user has not finished the current flag
- :param flag_arg_state: information about the unfinished flag action
- """
- error = "\nError: argument {}: {} ({} entered)\n".\
- format(argparse._get_action_name(flag_arg_state.action),
- generate_range_error(flag_arg_state.min, flag_arg_state.max),
- flag_arg_state.count)
- self._print_message(ansi.style_error('{}'.format(error)))
-
- def _print_completion_error(self, arg_action: argparse.Action, completion_error: CompletionError) -> None:
- """
- Print a CompletionError to the user
- :param arg_action: action being tab completed
- :param completion_error: error that occurred
- """
- # Indent all lines of completion_error
- indented_error = textwrap.indent(str(completion_error), ' ')
-
- error = ("\nError tab completing {}:\n"
- "{}\n".format(argparse._get_action_name(arg_action), indented_error))
- self._print_message(ansi.style_error('{}'.format(error)))
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index a59270c3..81fec013 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -26,7 +26,7 @@ value with no upper bound, use a 1-item tuple (min,)
parser.add_argument('-f', nargs=(3, 5))
Tab Completion:
- cmd2 uses its AutoCompleter class to enable argparse-based tab completion on all commands that use the
+ cmd2 uses its ArgparseCompleter class to enable argparse-based tab completion on all commands that use the
@with_argparse wrappers. Out of the box you get tab completion of commands, subcommands, and flag names,
as well as instructive hints about the current argument that print when tab is pressed. In addition,
you can add tab completion for each argument's values using parameters passed to add_argument().
@@ -53,7 +53,7 @@ Tab Completion:
choices_method
This is exactly like choices_function, but the function needs to be an instance method of a cmd2-based class.
- When AutoCompleter calls the method, it will pass the app instance as the self argument. This is good in
+ When ArgparseCompleter calls the method, it will pass the app instance as the self argument. This is good in
cases where the list of choices being generated relies on state data of the cmd2-based app
Example:
@@ -74,7 +74,7 @@ Tab Completion:
completer_method
This is exactly like completer_function, but the function needs to be an instance method of a cmd2-based class.
- When AutoCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides
+ When ArgparseCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides
a few completer methods for convenience (e.g., path_complete, delimiter_complete)
Example:
@@ -113,21 +113,14 @@ Tab Completion:
def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
All values of the arg_tokens dictionary are lists, even if a particular argument expects only 1 token. Since
- AutoCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate
+ ArgparseCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate
their values. All tokens are stored in the dictionary as the raw strings provided on the command line. It is up to
the developer to determine if the user entered the correct argument type (e.g. int) and validate their values.
-CompletionError Class:
- Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter
-
- Example use cases
- - Reading a database to retrieve a tab completion data set failed
- - A previous command line argument that determines the data set being completed is invalid
-
CompletionItem Class:
This class was added to help in cases where uninformative data is being tab completed. For instance,
tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems
- instead of a regular string for completion results will signal the AutoCompleter to output the completion
+ instead of a regular string for completion results will signal the ArgparseCompleter to output the completion
results in a table of completion tokens with descriptions instead of just a table of tokens.
Instead of this:
@@ -229,17 +222,6 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
return err_str
-class CompletionError(Exception):
- """
- Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter
-
- Example use cases
- - Reading a database to retrieve a tab completion data set failed
- - A previous command line argument that determines the data set being completed is invalid
- """
- pass
-
-
class CompletionItem(str):
"""
Completion item with descriptive text attached
@@ -353,15 +335,15 @@ def _add_argument_wrapper(self, *args,
:param nargs: extends argparse nargs functionality by allowing tuples which specify a range (min, max)
to specify a max value with no upper bound, use a 1-item tuple (min,)
- # Added args used by AutoCompleter
+ # Added args used by ArgparseCompleter
:param choices_function: function that provides choices for this argument
:param choices_method: cmd2-app method that provides choices for this argument
:param completer_function: tab completion function that provides choices for this argument
:param completer_method: cmd2-app tab completion method that provides choices for this argument
- :param suppress_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current
- argument's help text as a hint. Set this to True to suppress the hint. If this argument's
- help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the
- value passed for suppress_tab_hint. Defaults to False.
+ :param suppress_tab_hint: when ArgparseCompleter has no results to show during tab completion, it displays the
+ current argument's help text as a hint. Set this to True to suppress the hint. If this
+ argument's help text is set to argparse.SUPPRESS, then tab hints will not display
+ regardless of the value passed for suppress_tab_hint. Defaults to False.
:param descriptive_header: if the provided choices are CompletionItems, then this header will display
during tab completion. Defaults to None.
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 5a728e56..0bb4921e 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -47,13 +47,13 @@ from . import ansi
from . import constants
from . import plugin
from . import utils
-from .argparse_custom import CompletionError, CompletionItem, DEFAULT_ARGUMENT_PARSER
+from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .decorators import with_argparser
from .history import History, HistoryItem
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt, rl_warning
-from .utils import Settable
+from .utils import CompletionError, Settable
# Set up readline
if rl_type == RlType.NONE: # pragma: no cover
@@ -1416,17 +1416,27 @@ class Cmd(cmd.Cmd):
except IndexError:
return None
+ except CompletionError as ex:
+ # Don't print error and redraw the prompt unless the error has length
+ err_str = str(ex)
+ if err_str:
+ if ex.apply_style:
+ err_str = ansi.style_error(err_str)
+ ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
+ rl_force_redisplay()
+ return None
except Exception as e:
# Insert a newline so the exception doesn't print in the middle of the command line being tab completed
self.perror()
self.pexcept(e)
+ rl_force_redisplay()
return None
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int, *,
argparser: argparse.ArgumentParser, preserve_quotes: bool) -> List[str]:
"""Default completion function for argparse commands"""
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
# To have tab-completion parsing match command line parsing behavior,
@@ -2560,11 +2570,11 @@ class Cmd(cmd.Cmd):
if func is None or argparser is None:
return []
- # Combine the command and its subcommand tokens for the AutoCompleter
+ # Combine the command and its subcommand tokens for the ArgparseCompleter
tokens = [command] + arg_tokens['subcommands']
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
return completer.complete_subcommand_help(tokens, text, line, begidx, endidx)
help_parser = DEFAULT_ARGUMENT_PARSER(description="List available commands or provide "
@@ -2576,7 +2586,7 @@ class Cmd(cmd.Cmd):
help_parser.add_argument('-v', '--verbose', action='store_true',
help="print a list of all commands with descriptions of each")
- # Get rid of cmd's complete_help() functions so AutoCompleter will complete the help command
+ # Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
if getattr(cmd.Cmd, 'complete_help', None) is not None:
delattr(cmd.Cmd, 'complete_help')
@@ -2594,8 +2604,8 @@ class Cmd(cmd.Cmd):
# If the command function uses argparse, then use argparse's help
if func is not None and argparser is not None:
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(argparser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(argparser, self)
tokens = [args.command] + args.subcommands
# Set end to blank so the help output matches how it looks when "command -h" is used
@@ -2838,8 +2848,8 @@ class Cmd(cmd.Cmd):
completer_function=settable.completer_function,
completer_method=settable.completer_method)
- from .argparse_completer import AutoCompleter
- completer = AutoCompleter(settable_parser, self)
+ from .argparse_completer import ArgparseCompleter
+ completer = ArgparseCompleter(settable_parser, self)
# Use raw_tokens since quotes have been preserved
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
@@ -2860,7 +2870,7 @@ class Cmd(cmd.Cmd):
set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
# Suppress tab-completion hints for this field. The completer method is going to create an
- # AutoCompleter based on the actual parameter being completed and we only want that hint printing.
+ # ArgparseCompleter based on the actual parameter being completed and we only want that hint printing.
set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable',
completer_method=complete_set_value, suppress_tab_hint=True)
diff --git a/cmd2/utils.py b/cmd2/utils.py
index e324c2f1..6a67c43f 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -72,6 +72,31 @@ def str_to_bool(val: str) -> bool:
raise ValueError("must be True or False (case-insensitive)")
+class CompletionError(Exception):
+ """
+ Raised during tab completion operations to report any sort of error you want printed by the ArgparseCompleter
+ This can also be used just to display a message, even if it's not an error. ArgparseCompleter raises
+ CompletionErrors to display tab completion hints and sets apply_style to False so hints aren't colored
+ like error text.
+
+ Example use cases
+ - Reading a database to retrieve a tab completion data set failed
+ - A previous command line argument that determines the data set being completed is invalid
+ - Tab completion hints
+ """
+ def __init__(self, *args, apply_style: bool = True, **kwargs):
+ """
+ Initializer for CompletionError
+ :param apply_style: If True, then ansi.style_error will be applied to the message text when printed.
+ Set to False in cases where the message text already has the desired style.
+ Defaults to True.
+ """
+ self.apply_style = apply_style
+
+ # noinspection PyArgumentList
+ super().__init__(*args, **kwargs)
+
+
class Settable:
"""Used to configure a cmd2 instance member to be settable via the set command in the CLI"""
def __init__(self, name: str, val_type: Callable, description: str, *,
@@ -109,8 +134,8 @@ class Settable:
for this argument (See note below)
Note:
- For choices_method and completer_method, do not set them to a bound method. This is because AutoCompleter
- passes the self argument explicitly to these functions.
+ For choices_method and completer_method, do not set them to a bound method. This is because
+ ArgparseCompleter passes the self argument explicitly to these functions.
Therefore instead of passing something like self.path_complete, pass cmd2.Cmd.path_complete.
"""
diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py
index 90975d3f..bf2b2723 100644
--- a/examples/argparse_completion.py
+++ b/examples/argparse_completion.py
@@ -6,8 +6,8 @@ A simple example demonstrating how to integrate tab completion with argparse-bas
import argparse
from typing import Dict, List
-from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, CompletionError, CompletionItem
-from cmd2.utils import basic_complete
+from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, CompletionItem
+from cmd2.utils import basic_complete, CompletionError
# Data source for argparse.choices
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
diff --git a/examples/basic_completion.py b/examples/basic_completion.py
index e021828b..9523ac67 100755
--- a/examples/basic_completion.py
+++ b/examples/basic_completion.py
@@ -2,16 +2,18 @@
# coding=utf-8
"""
A simple example demonstrating how to enable tab completion by assigning a completer function to do_* commands.
-This also demonstrates capabilities of the following completer methods included with cmd2:
-- delimiter_completer
-- flag_based_complete (see note below)
-- index_based_complete (see note below)
+This also demonstrates capabilities of the following completer features included with cmd2:
+- CompletionError exceptions
+- delimiter_completer()
+- flag_based_complete() (see note below)
+- index_based_complete() (see note below)
flag_based_complete() and index_based_complete() are basic methods and should only be used if you are not
familiar with argparse. The recommended approach for tab completing positional tokens and flags is to use
argparse-based completion. For an example integrating tab completion with argparse, see argparse_completion.py
"""
import functools
+from typing import List
import cmd2
@@ -42,7 +44,7 @@ class BasicCompletion(cmd2.Cmd):
"""
self.poutput("Args: {}".format(statement.args))
- def complete_flag_based(self, text, line, begidx, endidx):
+ def complete_flag_based(self, text, line, begidx, endidx) -> List[str]:
"""Completion function for do_flag_based"""
flag_dict = \
{
@@ -65,7 +67,7 @@ class BasicCompletion(cmd2.Cmd):
"""Tab completes first 3 arguments using index_based_complete"""
self.poutput("Args: {}".format(statement.args))
- def complete_index_based(self, text, line, begidx, endidx):
+ def complete_index_based(self, text, line, begidx, endidx) -> List[str]:
"""Completion function for do_index_based"""
index_dict = \
{
@@ -84,6 +86,20 @@ class BasicCompletion(cmd2.Cmd):
complete_delimiter_complete = functools.partialmethod(cmd2.Cmd.delimiter_complete,
match_against=file_strs, delimiter='/')
+ def do_raise_error(self, statement: cmd2.Statement):
+ """Demonstrates effect of raising CompletionError"""
+ self.poutput("Args: {}".format(statement.args))
+
+ def complete_raise_error(self, text, line, begidx, endidx) -> List[str]:
+ """
+ CompletionErrors can be raised if an error occurs while tab completing.
+
+ Example use cases
+ - Reading a database to retrieve a tab completion data set failed
+ - A previous command line argument that determines the data set being completed is invalid
+ """
+ raise cmd2.CompletionError("This is how a CompletionError behaves")
+
if __name__ == '__main__':
import sys
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index 97c75ef3..9e635a42 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -9,8 +9,8 @@ from typing import List
import pytest
import cmd2
-from cmd2 import with_argparser, Cmd2ArgumentParser, CompletionError, CompletionItem
-from cmd2.utils import StdSim, basic_complete
+from cmd2 import with_argparser, Cmd2ArgumentParser, CompletionItem
+from cmd2.utils import CompletionError, StdSim, basic_complete
from .conftest import run_cmd, complete_tester
# Lists used in our tests (there is a mix of sorted and unsorted on purpose)
@@ -42,20 +42,20 @@ def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[s
def choices_takes_arg_tokens(arg_tokens: argparse.Namespace) -> List[str]:
- """Choices function that receives arg_tokens from AutoCompleter"""
+ """Choices function that receives arg_tokens from ArgparseCompleter"""
return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]]
def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int,
arg_tokens: argparse.Namespace) -> List[str]:
- """Completer function that receives arg_tokens from AutoCompleter"""
+ """Completer function that receives arg_tokens from ArgparseCompleter"""
match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]]
return basic_complete(text, line, begidx, endidx, match_against)
-# noinspection PyMethodMayBeStatic,PyUnusedLocal
+# noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember
class AutoCompleteTester(cmd2.Cmd):
- """Cmd2 app that exercises AutoCompleter class"""
+ """Cmd2 app that exercises ArgparseCompleter class"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -181,6 +181,7 @@ class AutoCompleteTester(cmd2.Cmd):
choices=one_or_more_choices)
nargs_parser.add_argument("--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL,
choices=optional_choices)
+ # noinspection PyTypeChecker
nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2),
choices=range_choices)
nargs_parser.add_argument("--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER,
@@ -421,7 +422,7 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
else:
assert first_match is None
- # Numbers will be sorted in ascending order and then converted to strings by AutoCompleter
+ # Numbers will be sorted in ascending order and then converted to strings by ArgparseCompleter
if all(isinstance(x, numbers.Number) for x in completions):
completions.sort()
completions = [str(x) for x in completions]
@@ -496,8 +497,8 @@ def test_autocomp_positional_completers(ac_app, pos, text, completions):
def test_autocomp_blank_token(ac_app):
- """Force a blank token to make sure AutoCompleter consumes them like argparse does"""
- from cmd2.argparse_completer import AutoCompleter
+ """Force a blank token to make sure ArgparseCompleter consumes them like argparse does"""
+ from cmd2.argparse_completer import ArgparseCompleter
blank = ''
@@ -507,7 +508,7 @@ def test_autocomp_blank_token(ac_app):
endidx = len(line)
begidx = endidx - len(text)
- completer = AutoCompleter(ac_app.completer_parser, ac_app)
+ completer = ArgparseCompleter(ac_app.completer_parser, ac_app)
tokens = ['completer', '-f', blank, text]
completions = completer.complete_command(tokens, text, line, begidx, endidx)
assert completions == completions_from_function
@@ -518,7 +519,7 @@ def test_autocomp_blank_token(ac_app):
endidx = len(line)
begidx = endidx - len(text)
- completer = AutoCompleter(ac_app.completer_parser, ac_app)
+ completer = ArgparseCompleter(ac_app.completer_parser, ac_app)
tokens = ['completer', blank, text]
completions = completer.complete_command(tokens, text, line, begidx, endidx)
assert completions == completions_from_method
@@ -699,7 +700,7 @@ def test_completion_items_default_header(ac_app):
('hint', '', True),
('hint --flag', '', True),
('hint --suppressed_help', '', False),
- ('hint --suppressed_hint', '--', False),
+ ('hint --suppressed_hint', '', False),
# Hint because flag does not have enough values to be considered finished
('nargs --one_or_more', '-', True),
@@ -730,7 +731,10 @@ def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys):
complete_tester(text, line, begidx, endidx, ac_app)
out, err = capsys.readouterr()
- assert has_hint == ("Hint:\n" in out)
+ if has_hint:
+ assert "Hint:\n" in out
+ else:
+ assert not out
def test_autocomp_hint_no_help_text(ac_app, capsys):
@@ -867,20 +871,20 @@ def test_looks_like_flag():
def test_complete_command_no_tokens(ac_app):
- from cmd2.argparse_completer import AutoCompleter
+ from cmd2.argparse_completer import ArgparseCompleter
parser = Cmd2ArgumentParser()
- ac = AutoCompleter(parser, ac_app)
+ ac = ArgparseCompleter(parser, ac_app)
completions = ac.complete_command(tokens=[], text='', line='', begidx=0, endidx=0)
assert not completions
def test_complete_command_help_no_tokens(ac_app):
- from cmd2.argparse_completer import AutoCompleter
+ from cmd2.argparse_completer import ArgparseCompleter
parser = Cmd2ArgumentParser()
- ac = AutoCompleter(parser, ac_app)
+ ac = ArgparseCompleter(parser, ac_app)
completions = ac.complete_subcommand_help(tokens=[], text='', line='', begidx=0, endidx=0)
assert not completions