Skip to content

Commit

Permalink
Merge branch 'master' into black
Browse files Browse the repository at this point in the history
# Conflicts:
#	cmd2/__init__.py
#	cmd2/argparse_completer.py
#	cmd2/argparse_custom.py
#	cmd2/cmd2.py
#	cmd2/decorators.py
#	cmd2/exceptions.py
#	cmd2/utils.py
#	examples/arg_decorators.py
#	examples/argparse_completion.py
#	examples/modular_commands_main.py
#	tests/test_argparse_completer.py
#	tests/test_argparse_custom.py
#	tests/test_cmd2.py
#	tests/test_completion.py
#	tests/test_history.py
  • Loading branch information
tleonhardt committed Feb 20, 2021
2 parents 4c70bdb + 06aaf96 commit 3e180a8
Show file tree
Hide file tree
Showing 28 changed files with 1,010 additions and 1,109 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## 2.0.0 (TBD, 2021)
* Breaking changes
* Argparse Completion / Settables
* Replaced `choices_function` / `choices_method` with `choices_provider`.
* Replaced `completer_function` / `completer_method` with `completer`.
* ArgparseCompleter now always passes `cmd2.Cmd` or `CommandSet` instance as the first positional
argument to choices_provider and completer functions.
* Moved `basic_complete` from utils into `cmd2.Cmd` class.
* Moved `CompletionError` to exceptions.py
* ``Namespace.__statement__`` has been removed. Use `Namespace.cmd2_statement.get()` instead.
* Removed `--silent` flag from `alias/macro create` since startup scripts can be run silently.
* Removed `--with_silent` flag from `alias/macro list` since startup scripts can be run silently.
* Removed `with_argparser_and_unknown_args` since it was deprecated in 1.3.0.
* Enhancements
* Added support for custom tab completion and up-arrow input history to `cmd2.Cmd2.read_input`.
See [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py)
for an example.

## 1.5.0 (January 31, 2021)
* Bug Fixes
* Fixed bug where setting `always_show_hint=True` did not show a hint when completing `Settables`
Expand Down
6 changes: 3 additions & 3 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
from .cmd2 import Cmd
from .command_definition import CommandSet, with_default_category
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
from .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category, as_subcommand_to
from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks, CommandSetRegistrationError
from .decorators import with_argument_list, with_argparser, with_category, as_subcommand_to
from .exceptions import Cmd2ArgparseError, CommandSetRegistrationError, CompletionError, SkipPostcommandHooks
from . import plugin
from .parsing import Statement
from .py_bridge import CommandResult
from .utils import categorize, CompletionError, Settable
from .utils import categorize, CompletionMode, CustomCompletionSettings, Settable
96 changes: 45 additions & 51 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@
from .command_definition import (
CommandSet,
)
from .exceptions import (
CompletionError,
)
from .table_creator import (
Column,
SimpleTable,
)
from .utils import (
CompletionError,
basic_complete,
)

# If no descriptive header is supplied, then this will be used instead
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
Expand Down Expand Up @@ -209,11 +208,19 @@ def __init__(
if isinstance(action, argparse._SubParsersAction):
self._subcommand_action = action

def complete_command(
self, tokens: List[str], text: str, line: str, begidx: int, endidx: int, *, cmd_set: Optional[CommandSet] = None
) -> List[str]:
def complete(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *,
cmd_set: Optional[CommandSet] = None) -> List[str]:
"""
Complete the command using the argparse metadata and provided argument dictionary
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:
Expand Down Expand Up @@ -290,7 +297,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
#############################################################################################
# Parse all but the last token
#############################################################################################
for token_index, token in enumerate(tokens[1:-1], start=1):
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:
Expand Down Expand Up @@ -384,12 +391,10 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
if action.dest != argparse.SUPPRESS:
parent_tokens[action.dest] = [token]

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, cmd_set=cmd_set
)
completer = ArgparseCompleter(self._subcommand_action.choices[token], 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 []
Expand Down Expand Up @@ -433,9 +438,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:

# Check if we are completing a flag's argument
if flag_arg_state is not None:
completion_results = self._complete_for_arg(
flag_arg_state, text, line, begidx, endidx, consumed_arg_values, cmd_set=cmd_set
)
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:
Expand All @@ -460,9 +464,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
action = remaining_positionals.popleft()
pos_arg_state = _ArgumentState(action)

completion_results = self._complete_for_arg(
pos_arg_state, text, line, begidx, endidx, consumed_arg_values, cmd_set=cmd_set
)
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:
Expand Down Expand Up @@ -496,7 +499,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
if action.help != argparse.SUPPRESS:
match_against.append(flag)

matches = basic_complete(text, line, begidx, endidx, match_against)
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() # type: Dict[argparse.Action, List[str]]
Expand Down Expand Up @@ -568,58 +571,50 @@ def _format_completions(self, arg_state: _ArgumentState, completions: List[Union

return completions

def complete_subcommand_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
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 tokens: command line tokens
: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[1:], start=1):
for token_index, token in enumerate(tokens):
if token in self._subcommand_action.choices:
completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
return completer.complete_subcommand_help(tokens[token_index:], text, line, begidx, endidx)
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 basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
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: command line tokens
: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[1:], start=1):
for token_index, token in enumerate(tokens):
if token in self._subcommand_action.choices:
completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
return completer.format_help(tokens[token_index:])
return completer.format_help(tokens[token_index + 1:])
else:
break
return self._parser.format_help()

def _complete_for_arg(
self,
arg_state: _ArgumentState,
text: str,
line: str,
begidx: int,
endidx: int,
consumed_arg_values: Dict[str, List[str]],
*,
cmd_set: Optional[CommandSet] = None
) -> List[str]:
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
Expand Down Expand Up @@ -647,16 +642,15 @@ def _complete_for_arg(
args = []
kwargs = {}
if isinstance(arg_choices, ChoicesCallable):
if arg_choices.is_method:
# 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
cmd_set = self._cmd2_app._resolve_func_self(arg_choices.to_call, cmd_set)
if cmd_set is None:
# No cases matched, raise an error
raise CompletionError('Could not find CommandSet instance matching defining type for completer')
# 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(cmd_set)
args.append(self_arg)

# Check if arg_choices.to_call expects arg_tokens
to_call_params = inspect.signature(arg_choices.to_call).parameters
Expand Down Expand Up @@ -687,7 +681,7 @@ def _complete_for_arg(
arg_choices = [choice for choice in arg_choices if choice not in used_values]

# Do tab completion on the choices
results = basic_complete(text, line, begidx, endidx, arg_choices)
results = self._cmd2_app.basic_complete(text, line, begidx, endidx, arg_choices)

if not results:
# Reset the value for matches_sorted. This is because completion of flag names
Expand Down
Loading

0 comments on commit 3e180a8

Please sign in to comment.