Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ prompt is displayed.
before calling it like the previous functions did.
- Removed `Cmd.default_to_shell`.
- Removed `Cmd.ruler` since `cmd2` no longer uses it.
- All parsers used with `cmd2` commands must be an instance of `Cmd2ArgumentParser` or a child
class of it.
- Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `ap_completer_type` is
now a public member of `Cmd2ArgumentParser`.
- Enhancements
- New `cmd2.Cmd` parameters
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These
Expand Down
124 changes: 67 additions & 57 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,11 @@
cast,
)

from rich.text import Text

from .constants import INFINITY
from .rich_utils import Cmd2SimpleTable

if TYPE_CHECKING: # pragma: no cover
from .cmd2 import Cmd

from rich.table import Column
from rich.text import Text

from .argparse_custom import (
ChoicesCallable,
Cmd2ArgumentParser,
generate_range_error,
)
from .command_definition import CommandSet
Expand All @@ -42,14 +35,25 @@
Completions,
all_display_numeric,
)
from .constants import INFINITY
from .exceptions import CompletionError
from .rich_utils import Cmd2SimpleTable
from .types import (
ChoicesProviderUnbound,
CmdOrSet,
CompleterUnbound,
)

if TYPE_CHECKING: # pragma: no cover
from .cmd2 import Cmd


# 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'


def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str:
def _build_hint(parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> str:
"""Build 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]
Expand All @@ -64,12 +68,12 @@ def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) ->
return formatter.format_help()


def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool:
def _single_prefix_char(token: str, parser: Cmd2ArgumentParser) -> bool:
"""Is a token just a single flag prefix character."""
return len(token) == 1 and token[0] in parser.prefix_chars


def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
def _looks_like_flag(token: str, parser: Cmd2ArgumentParser) -> 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
Expand Down Expand Up @@ -140,12 +144,12 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None:


class _NoResultsError(CompletionError):
def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
def __init__(self, parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> None:
"""CompletionError which occurs when there are no results.

If hinting is allowed on this argument, then its hint text will display.

:param parser: ArgumentParser instance which owns the action being completed
:param parser: Cmd2ArgumentParser instance which owns the action being completed
:param arg_action: action being completed.
"""
# Set apply_style to False because we don't want hints to look like errors
Expand All @@ -157,14 +161,14 @@ class ArgparseCompleter:

def __init__(
self,
parser: argparse.ArgumentParser,
parser: Cmd2ArgumentParser,
cmd2_app: 'Cmd',
*,
parent_tokens: Mapping[str, MutableSequence[str]] | None = None,
) -> None:
"""Create an ArgparseCompleter.

:param parser: ArgumentParser instance
:param parser: Cmd2ArgumentParser instance
:param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter
:param parent_tokens: optional Mapping of parent parsers' arg names to their tokens
This is only used by ArgparseCompleter when recursing on subcommand parsers
Expand All @@ -187,7 +191,7 @@ def __init__(
self._positional_actions: list[argparse.Action] = []

# This will be set if self._parser has subcommands
self._subcommand_action: argparse._SubParsersAction[argparse.ArgumentParser] | None = None
self._subcommand_action: argparse._SubParsersAction[Cmd2ArgumentParser] | None = None

# Start digging through the argparse structures.
# _actions is the top level container of parameter definitions
Expand Down Expand Up @@ -707,33 +711,32 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None
return
self._parser.print_help(file=file)

def _get_raw_choices(self, arg_state: _ArgumentState) -> list[CompletionItem] | ChoicesCallable | None:
"""Extract choices from action or return the choices_callable."""
if arg_state.action.choices is not None:
# If choices are subcommands, then get their help text to populate display_meta.
if isinstance(arg_state.action, argparse._SubParsersAction):
parser_help = {}
for action in arg_state.action._choices_actions:
if action.dest in arg_state.action.choices:
subparser = arg_state.action.choices[action.dest]
parser_help[subparser] = action.help or ''

return [
CompletionItem(name, display_meta=parser_help.get(subparser, ''))
for name, subparser in arg_state.action.choices.items()
]

# Standard choices
def _choices_to_items(self, arg_state: _ArgumentState) -> list[CompletionItem]:
"""Convert choices from action to list of CompletionItems."""
if arg_state.action.choices is None:
return []

# If choices are subcommands, then get their help text to populate display_meta.
if isinstance(arg_state.action, argparse._SubParsersAction):
parser_help = {}
for action in arg_state.action._choices_actions:
if action.dest in arg_state.action.choices:
subparser = arg_state.action.choices[action.dest]
parser_help[subparser] = action.help or ''

return [
choice if isinstance(choice, CompletionItem) else CompletionItem(choice) for choice in arg_state.action.choices
CompletionItem(name, display_meta=parser_help.get(subparser, ''))
for name, subparser in arg_state.action.choices.items()
]

choices_callable: ChoicesCallable | None = arg_state.action.get_choices_callable() # type: ignore[attr-defined]
return choices_callable
# Standard choices
return [
choice if isinstance(choice, CompletionItem) else CompletionItem(choice) for choice in arg_state.action.choices
]

def _prepare_callable_params(
self,
choices_callable: ChoicesCallable,
to_call: ChoicesProviderUnbound[CmdOrSet] | CompleterUnbound[CmdOrSet],
arg_state: _ArgumentState,
text: str,
consumed_arg_values: dict[str, list[str]],
Expand All @@ -744,14 +747,14 @@ def _prepare_callable_params(
kwargs: dict[str, Any] = {}

# Resolve the 'self' instance for the method
self_arg = self._cmd2_app._resolve_func_self(choices_callable.to_call, cmd_set)
self_arg = self._cmd2_app._resolve_func_self(to_call, cmd_set)
if self_arg is None:
raise CompletionError("Could not find CommandSet instance matching defining type for completer")
raise CompletionError("Could not find CommandSet instance matching defining type")

args.append(self_arg)

# Check if the function expects 'arg_tokens'
to_call_params = inspect.signature(choices_callable.to_call).parameters
to_call_params = inspect.signature(to_call).parameters
if ARG_TOKENS in to_call_params:
arg_tokens = {**self._parent_tokens, **consumed_arg_values}
arg_tokens.setdefault(arg_state.action.dest, []).append(text)
Expand All @@ -775,26 +778,33 @@ def _complete_arg(
:return: a Completions object
:raises CompletionError: if the completer or choices function this calls raises one
"""
raw_choices = self._get_raw_choices(arg_state)
if not raw_choices:
return Completions()

# Check if the argument uses a completer function
if isinstance(raw_choices, ChoicesCallable) and raw_choices.is_completer:
args, kwargs = self._prepare_callable_params(raw_choices, arg_state, text, consumed_arg_values, cmd_set)
# Check if the argument uses a completer
completer = arg_state.action.get_completer() # type: ignore[attr-defined]
if completer is not None:
args, kwargs = self._prepare_callable_params(
completer,
arg_state,
text,
consumed_arg_values,
cmd_set,
)
args.extend([text, line, begidx, endidx])
completions = raw_choices.completer(*args, **kwargs)
completions: Completions = completer(*args, **kwargs)

# Otherwise it uses a choices list or choices provider function
# Otherwise it uses a choices provider or choices list
else:
all_choices: list[CompletionItem] = []

if isinstance(raw_choices, ChoicesCallable):
args, kwargs = self._prepare_callable_params(raw_choices, arg_state, text, consumed_arg_values, cmd_set)
choices_func = raw_choices.choices_provider
all_choices = list(choices_func(*args, **kwargs))
choices_provider = arg_state.action.get_choices_provider() # type: ignore[attr-defined]
if choices_provider is not None:
args, kwargs = self._prepare_callable_params(
choices_provider,
arg_state,
text,
consumed_arg_values,
cmd_set,
)
all_choices = list(choices_provider(*args, **kwargs))
else:
all_choices = raw_choices
all_choices = self._choices_to_items(arg_state)

# Filter used values and run basic completion
used_values = consumed_arg_values.get(arg_state.action.dest, [])
Expand Down
Loading
Loading