summaryrefslogtreecommitdiff
path: root/cmd2/argparse_completer.py
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2020-02-18 19:47:25 -0500
committerGitHub <noreply@github.com>2020-02-18 19:47:25 -0500
commit970c5fc42c69c1c03640c979df341e85e3c38848 (patch)
treed14b6c335ee6f5bfa6fbb2d509846cdbff23e98c /cmd2/argparse_completer.py
parent2221e08e996a34660125536e3fc34eb231b3b060 (diff)
parent34ce17c95fae0a849cd90de2e65cd454b9fe51cb (diff)
downloadcmd2-git-970c5fc42c69c1c03640c979df341e85e3c38848.tar.gz
Merge pull request #892 from python-cmd2/completion_updates
Completion updates
Diffstat (limited to 'cmd2/argparse_completer.py')
-rw-r--r--cmd2/argparse_completer.py257
1 files changed, 112 insertions, 145 deletions
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)))