impuls/lib/python3.11/site-packages/cmd2/argparse_completer.py

794 lines
36 KiB
Python
Raw Normal View History

# coding=utf-8
# flake8: noqa C901
# NOTE: Ignoring flake8 cyclomatic complexity in this file
"""
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.
"""
import argparse
import inspect
import numbers
from collections import (
deque,
)
from typing import (
TYPE_CHECKING,
Dict,
List,
Optional,
Type,
Union,
cast,
)
from .ansi import (
style_aware_wcswidth,
widest_line,
)
from .constants import (
INFINITY,
)
if TYPE_CHECKING: # pragma: no cover
from .cmd2 import (
Cmd,
)
from .argparse_custom import (
ChoicesCallable,
ChoicesProviderFuncWithTokens,
CompletionItem,
generate_range_error,
)
from .command_definition import (
CommandSet,
)
from .exceptions import (
CompletionError,
)
from .table_creator import (
Column,
HorizontalAlignment,
SimpleTable,
)
# If no descriptive header is supplied, then this will be used instead
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
# Name of the choice/completer function argument that, if present, will be passed a dictionary of
# command line tokens up through the token being completed mapped to their argparse destination name.
ARG_TOKENS = 'arg_tokens'
# noinspection PyProtectedMember
def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str:
"""Build tab completion hint for a given argument"""
# Check if hinting is disabled for this argument
suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined]
if suppress_hint or arg_action.help == argparse.SUPPRESS:
return ''
else:
# Use the parser's help formatter to display just this action's help text
formatter = parser._get_formatter()
formatter.start_section("Hint")
formatter.add_argument(arg_action)
formatter.end_section()
return formatter.format_help()
def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool:
"""Returns if a token is just a single flag prefix character"""
return len(token) == 1 and token[0] in parser.prefix_chars
# noinspection PyProtectedMember
def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
"""
Determine if a token looks like a flag. Unless an argument has nargs set to argparse.REMAINDER,
then anything that looks like a flag can't be consumed as a value for it.
Based on argparse._parse_optional().
"""
# Flags have to be at least characters
if len(token) < 2:
return False
# Flags have to start with a prefix character
if not token[0] in parser.prefix_chars:
return False
# If it looks like a negative number, it is not a flag unless there are negative-number-like flags
if parser._negative_number_matcher.match(token):
if not parser._has_negative_number_optionals:
return False
# Flags can't have a space
if ' ' in token:
return False
# Starts like a flag
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: Union[int, str]
self.max: Union[float, int, str]
self.count = 0
self.is_remainder = self.action.nargs == argparse.REMAINDER
# Check if nargs is a range
nargs_range = self.action.get_nargs_range() # type: ignore[attr-defined]
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 _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(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max)),
flag_arg_state.count,
)
super().__init__(error)
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
"""
# Set apply_style to False because we don't want hints to look like errors
super().__init__(_build_hint(parser, arg_action), apply_style=False)
# noinspection PyProtectedMember
class ArgparseCompleter:
"""Automatic command line tab completion based on argparse parameters"""
def __init__(
self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None
) -> None:
"""
Create an ArgparseCompleter
:param parser: ArgumentParser instance
: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 ArgparseCompleter when recursing on subcommand parsers
Defaults to None
"""
self._parser = parser
self._cmd2_app = cmd2_app
if parent_tokens is None:
parent_tokens = dict()
self._parent_tokens = parent_tokens
self._flags = [] # all flags in this command
self._flag_to_action = {} # maps flags to the argparse action object
self._positional_actions = [] # actions for positional arguments (by position index)
self._subcommand_action = None # this will be set if self._parser has subcommands
# Start digging through the argparse structures.
# _actions is the top level container of parameter definitions
for action in self._parser._actions:
# if the parameter is flag based, it will have option_strings
if action.option_strings:
# record each option flag
for option in action.option_strings:
self._flags.append(option)
self._flag_to_action[option] = action
# Otherwise this is a positional parameter
else:
self._positional_actions.append(action)
# Check if this action defines subcommands
if isinstance(action, argparse._SubParsersAction):
self._subcommand_action = action
def complete(
self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *, cmd_set: Optional[CommandSet] = None
) -> List[str]:
"""
Complete text using argparse metadata
:param text: the string prefix we are attempting to match (all matches must begin with it)
:param line: the current input line with leading whitespace removed
:param begidx: the beginning index of the prefix text
:param endidx: the ending index of the prefix text
:param tokens: list of argument tokens being passed to the parser
:param cmd_set: if tab completing a command, the CommandSet the command's function belongs to, if applicable.
Defaults to None.
:raises: CompletionError for various types of tab completion errors
"""
if not tokens:
return []
# Positionals args that are left to parse
remaining_positionals = deque(self._positional_actions)
# This gets set to True when flags will no longer be processed as argparse flags
# That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
skip_remaining_flags = False
# _ArgumentState of the current positional
pos_arg_state: Optional[_ArgumentState] = None
# _ArgumentState of the current flag
flag_arg_state: Optional[_ArgumentState] = None
# Non-reusable flags that we've parsed
matched_flags: List[str] = []
# Keeps track of arguments we've seen and any tokens they consumed
consumed_arg_values: Dict[str, List[str]] = dict() # dict(arg_name -> List[tokens])
# Completed mutually exclusive groups
completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()
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) -> 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
: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:
if arg_action in group._group_actions:
# Check if the group this action belongs to has already been completed
if group in completed_mutex_groups:
# If this is the action that completed the group, then there is no error
# 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
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
# Don't tab complete any of the other args in the group
for group_action in group._group_actions:
if group_action == arg_action:
continue
elif group_action in self._flag_to_action.values():
matched_flags.extend(group_action.option_strings)
elif group_action in remaining_positionals:
remaining_positionals.remove(group_action)
# Arg can only be in one group, so we are done
break
#############################################################################################
# Parse all but the last token
#############################################################################################
for token_index, token in enumerate(tokens[:-1]):
# If we're in a positional REMAINDER arg, force all future tokens to go to that
if pos_arg_state is not None and pos_arg_state.is_remainder:
consume_argument(pos_arg_state)
continue
# If we're in a flag REMAINDER arg, force all future tokens to go to that until a double dash is hit
elif flag_arg_state is not None and flag_arg_state.is_remainder:
if token == '--':
flag_arg_state = None
else:
consume_argument(flag_arg_state)
continue
# Handle '--' which tells argparse all remaining arguments are non-flags
elif token == '--' and not skip_remaining_flags:
# Check if there is an unfinished flag
if (
flag_arg_state is not None
and isinstance(flag_arg_state.min, int)
and flag_arg_state.count < flag_arg_state.min
):
raise _UnfinishedFlagError(flag_arg_state)
# Otherwise end the current flag
else:
flag_arg_state = None
skip_remaining_flags = True
continue
# Check the format of the current token to see if it can be an argument's value
if _looks_like_flag(token, self._parser) and not skip_remaining_flags:
# Check if there is an unfinished flag
if (
flag_arg_state is not None
and isinstance(flag_arg_state.min, int)
and flag_arg_state.count < flag_arg_state.min
):
raise _UnfinishedFlagError(flag_arg_state)
# Reset flag arg state but not positional tracking because flags can be
# interspersed anywhere between positionals
flag_arg_state = None
action = None
# Does the token match a known flag?
if token in self._flag_to_action:
action = self._flag_to_action[token]
elif self._parser.allow_abbrev:
candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)]
if len(candidates_flags) == 1:
action = self._flag_to_action[candidates_flags[0]]
if action is not None:
update_mutex_groups(action)
if isinstance(action, (argparse._AppendAction, argparse._AppendConstAction, argparse._CountAction)):
# Flags with action set to append, append_const, and count can be reused
# Therefore don't erase any tokens already consumed for this flag
consumed_arg_values.setdefault(action.dest, [])
else:
# This flag is not reusable, so mark that we've seen it
matched_flags.extend(action.option_strings)
# It's possible we already have consumed values for this flag if it was used
# earlier in the command line. Reset them now for this use of it.
consumed_arg_values[action.dest] = []
new_arg_state = _ArgumentState(action)
# Keep track of this flag if it can receive arguments
if new_arg_state.max > 0: # type: ignore[operator]
flag_arg_state = new_arg_state
skip_remaining_flags = flag_arg_state.is_remainder
# Check if we are consuming a flag
elif flag_arg_state is not None:
consume_argument(flag_arg_state)
# Check if we have finished with this flag
if isinstance(flag_arg_state.max, (float, int)) and flag_arg_state.count >= flag_arg_state.max:
flag_arg_state = None
# Otherwise treat as a positional argument
else:
# If we aren't current tracking a positional, then get the next positional arg to handle this token
if pos_arg_state is None:
# Make sure we are still have positional arguments to parse
if remaining_positionals:
action = remaining_positionals.popleft()
# Are we at a subcommand? If so, forward to the matching completer
if action == self._subcommand_action:
if token in self._subcommand_action.choices:
# Merge self._parent_tokens and consumed_arg_values
parent_tokens = {**self._parent_tokens, **consumed_arg_values}
# Include the subcommand name if its destination was set
if action.dest != argparse.SUPPRESS:
parent_tokens[action.dest] = [token]
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
completer_type = self._cmd2_app._determine_ap_completer_type(parser)
completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens)
return completer.complete(
text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set
)
else:
# Invalid subcommand entered, so no way to complete remaining tokens
return []
# Otherwise keep track of the argument
else:
pos_arg_state = _ArgumentState(action)
# Check if we have a positional to consume this token
if pos_arg_state is not None:
update_mutex_groups(pos_arg_state.action)
consume_argument(pos_arg_state)
# No more flags are allowed if this is a REMAINDER argument
if pos_arg_state.is_remainder:
skip_remaining_flags = True
# Check if we have finished with this positional
elif isinstance(pos_arg_state.max, (float, int)) and pos_arg_state.count >= pos_arg_state.max:
pos_arg_state = None
# Check if the next positional has nargs set to argparse.REMAINDER.
# At this point argparse allows no more flags to be processed.
if remaining_positionals and remaining_positionals[0].nargs == argparse.REMAINDER:
skip_remaining_flags = True
#############################################################################################
# We have parsed all but the last token and have enough information to complete it
#############################################################################################
# Check if we are completing a flag name. This check ignores strings with a length of one, like '-'.
# This is because that could be the start of a negative number which may be a valid completion for
# the current argument. We will handle the completion of flags that start with only one prefix
# 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 isinstance(flag_arg_state.min, int)
and flag_arg_state.count < flag_arg_state.min
):
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:
completion_results = self._complete_arg(
text, line, begidx, endidx, flag_arg_state, consumed_arg_values, cmd_set=cmd_set
)
# If we have results, then return them
if completion_results:
# Don't overwrite an existing hint
if not self._cmd2_app.completion_hint:
self._cmd2_app.completion_hint = _build_hint(self._parser, flag_arg_state.action)
return completion_results
# Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag
elif (
(isinstance(flag_arg_state.min, int) and flag_arg_state.count < flag_arg_state.min)
or not _single_prefix_char(text, self._parser)
or skip_remaining_flags
):
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:
# 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 = _ArgumentState(action)
completion_results = self._complete_arg(
text, line, begidx, endidx, pos_arg_state, consumed_arg_values, cmd_set=cmd_set
)
# If we have results, then return them
if completion_results:
# Don't overwrite an existing hint
if not self._cmd2_app.completion_hint:
self._cmd2_app.completion_hint = _build_hint(self._parser, pos_arg_state.action)
return completion_results
# 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:
raise _NoResultsError(self._parser, pos_arg_state.action)
# If we aren't skipping remaining flags, then complete flag names if either is True:
# 1. text is a single flag prefix character that didn't complete against any argument values
# 2. there are no more positionals to complete
if not skip_remaining_flags and (_single_prefix_char(text, self._parser) or not remaining_positionals):
# Reset any completion settings that may have been set by functions which actually had no matches.
# Otherwise, those settings could alter how the flags are displayed.
self._cmd2_app._reset_completion_defaults()
return self._complete_flags(text, line, begidx, endidx, matched_flags)
return completion_results
def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
"""Tab completion routine for a parsers unused flags"""
# Build a list of flags that can be tab completed
match_against = []
for flag in self._flags:
# Make sure this flag hasn't already been used
if flag not in matched_flags:
# Make sure this flag isn't considered hidden
action = self._flag_to_action[flag]
if action.help != argparse.SUPPRESS:
match_against.append(flag)
matches = self._cmd2_app.basic_complete(text, line, begidx, endidx, match_against)
# Build a dictionary linking actions with their matched flag names
matched_actions: Dict[argparse.Action, List[str]] = dict()
for flag in matches:
action = self._flag_to_action[flag]
matched_actions.setdefault(action, [])
matched_actions[action].append(flag)
# For tab completion suggestions, group matched flags by action
for action, option_strings in matched_actions.items():
flag_text = ', '.join(option_strings)
# Mark optional flags with brackets
if not action.required:
flag_text = '[' + flag_text + ']'
self._cmd2_app.display_matches.append(flag_text)
return matches
def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
"""Format CompletionItems into hint table"""
# Nothing to do if we don't have at least 2 completions which are all CompletionItems
if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions):
return cast(List[str], completions)
completion_items = cast(List[CompletionItem], completions)
# Check if the data being completed have a numerical type
all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items)
# Sort CompletionItems before building the hint table
if not self._cmd2_app.matches_sorted:
# If all orig_value types are numbers, then sort by that value
if all_nums:
completion_items.sort(key=lambda c: c.orig_value) # type: ignore[no-any-return]
# Otherwise sort as strings
else:
completion_items.sort(key=self._cmd2_app.default_sort_key)
self._cmd2_app.matches_sorted = True
# Check if there are too many CompletionItems to display as a table
if len(completions) <= self._cmd2_app.max_completion_items:
four_spaces = 4 * ' '
# If a metavar was defined, use that instead of the dest field
destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest
# Handle case where metavar was a tuple
if isinstance(destination, tuple):
# Figure out what string in the tuple to use based on how many of the arguments have been completed.
# Use min() to avoid going passed the end of the tuple to support nargs being ZERO_OR_MORE and
# ONE_OR_MORE. In those cases, argparse limits metavar tuple to 2 elements but we may be completing
# the 3rd or more argument here.
tuple_index = min(len(destination) - 1, arg_state.count)
destination = destination[tuple_index]
desc_header = arg_state.action.get_descriptive_header() # type: ignore[attr-defined]
if desc_header is None:
desc_header = DEFAULT_DESCRIPTIVE_HEADER
# Replace tabs with 4 spaces so we can calculate width
desc_header = desc_header.replace('\t', four_spaces)
# Calculate needed widths for the token and description columns of the table
token_width = style_aware_wcswidth(destination)
desc_width = widest_line(desc_header)
for item in completion_items:
token_width = max(style_aware_wcswidth(item), token_width)
# Replace tabs with 4 spaces so we can calculate width
item.description = item.description.replace('\t', four_spaces)
desc_width = max(widest_line(item.description), desc_width)
cols = list()
dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT
cols.append(
Column(
destination.upper(),
width=token_width,
header_horiz_align=dest_alignment,
data_horiz_align=dest_alignment,
)
)
cols.append(Column(desc_header, width=desc_width))
hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)
table_data = [[item, item.description] for item in completion_items]
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
# Return sorted list of completions
return cast(List[str], completions)
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:
"""
Supports cmd2's help command in the completion of subcommand names
:param text: the string prefix we are attempting to match (all matches must begin with it)
:param line: the current input line with leading whitespace removed
:param begidx: the beginning index of the prefix text
:param endidx: the ending index of the prefix text
:param tokens: arguments passed to command/subcommand
: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 ArgparseCompleter.
if self._subcommand_action is not None:
for token_index, token in enumerate(tokens):
if token in self._subcommand_action.choices:
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
completer_type = self._cmd2_app._determine_ap_completer_type(parser)
completer = completer_type(parser, self._cmd2_app)
return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :])
elif token_index == len(tokens) - 1:
# Since this is the last token, we will attempt to complete it
return self._cmd2_app.basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
else:
break
return []
def format_help(self, tokens: List[str]) -> str:
"""
Supports cmd2's help command in the retrieval of help text
:param tokens: arguments passed to help command
: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 ArgparseCompleter.
if self._subcommand_action is not None:
for token_index, token in enumerate(tokens):
if token in self._subcommand_action.choices:
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
completer_type = self._cmd2_app._determine_ap_completer_type(parser)
completer = completer_type(parser, self._cmd2_app)
return completer.format_help(tokens[token_index + 1 :])
else:
break
return self._parser.format_help()
def _complete_arg(
self,
text: str,
line: str,
begidx: int,
endidx: int,
arg_state: _ArgumentState,
consumed_arg_values: Dict[str, List[str]],
*,
cmd_set: Optional[CommandSet] = None,
) -> List[str]:
"""
Tab completion routine for an argparse argument
:return: list of completions
:raises: CompletionError if the completer or choices function this calls raises one
"""
# Check if the arg provides choices to the user
arg_choices: Union[List[str], ChoicesCallable]
if arg_state.action.choices is not None:
arg_choices = list(arg_state.action.choices)
if not arg_choices:
return []
# If these choices are numbers, then sort them now
if all(isinstance(x, numbers.Number) for x in arg_choices):
arg_choices.sort()
self._cmd2_app.matches_sorted = True
# Since choices can be various types, make sure they are all strings
for index, choice in enumerate(arg_choices):
# Prevent converting anything that is already a str (i.e. CompletionItem)
if not isinstance(choice, str):
arg_choices[index] = str(choice) # type: ignore[unreachable]
else:
choices_attr = arg_state.action.get_choices_callable() # type: ignore[attr-defined]
if choices_attr is None:
return []
arg_choices = choices_attr
# If we are going to call a completer/choices function, then set up the common arguments
args = []
kwargs = {}
if isinstance(arg_choices, ChoicesCallable):
# The completer may or may not be defined in the same class as the command. Since completer
# functions are registered with the command argparser before anything is instantiated, we
# need to find an instance at runtime that matches the types during declaration
self_arg = self._cmd2_app._resolve_func_self(arg_choices.to_call, cmd_set)
if self_arg is None:
# No cases matched, raise an error
raise CompletionError('Could not find CommandSet instance matching defining type for completer')
args.append(self_arg)
# Check if arg_choices.to_call expects arg_tokens
to_call_params = inspect.signature(arg_choices.to_call).parameters
if ARG_TOKENS in to_call_params:
# Merge self._parent_tokens and consumed_arg_values
arg_tokens = {**self._parent_tokens, **consumed_arg_values}
# Include the token being completed
arg_tokens.setdefault(arg_state.action.dest, [])
arg_tokens[arg_state.action.dest].append(text)
# Add the namespace to the keyword arguments for the function we are calling
kwargs[ARG_TOKENS] = arg_tokens
# Check if the argument uses a specific tab completion function to provide its choices
if isinstance(arg_choices, ChoicesCallable) and arg_choices.is_completer:
args.extend([text, line, begidx, endidx])
results = arg_choices.completer(*args, **kwargs) # type: ignore[arg-type]
# Otherwise use basic_complete on the choices
else:
# Check if the choices come from a function
completion_items: List[str] = []
if isinstance(arg_choices, ChoicesCallable):
if not arg_choices.is_completer:
choices_func = arg_choices.choices_provider
if isinstance(choices_func, ChoicesProviderFuncWithTokens):
completion_items = choices_func(*args, **kwargs) # type: ignore[arg-type]
else: # pragma: no cover
# This won't hit because runtime checking doesn't check function argument types and will always
# resolve true above. Mypy, however, does see the difference and gives an error that can't be
# ignored. Mypy issue #5485 discusses this problem
completion_items = choices_func(*args) # type: ignore[arg-type]
# else case is already covered above
else:
completion_items = arg_choices
# Filter out arguments we already used
used_values = consumed_arg_values.get(arg_state.action.dest, [])
completion_items = [choice for choice in completion_items if choice not in used_values]
# Do tab completion on the choices
results = self._cmd2_app.basic_complete(text, line, begidx, endidx, completion_items)
if not results:
# Reset the value for matches_sorted. This is because completion of flag names
# may still be attempted after we return and they haven't been sorted yet.
self._cmd2_app.matches_sorted = False
return []
return self._format_completions(arg_state, results)
# The default ArgparseCompleter class for a cmd2 app
DEFAULT_AP_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter
def set_default_ap_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
"""
Set the default ArgparseCompleter class for a cmd2 app.
:param completer_type: Type that is a subclass of ArgparseCompleter.
"""
global DEFAULT_AP_COMPLETER
DEFAULT_AP_COMPLETER = completer_type