Skip to content

Commit

Permalink
Resolves comments from PR
Browse files Browse the repository at this point in the history
  • Loading branch information
anselor committed Mar 18, 2021
1 parent 9d1b7c7 commit 8f981f3
Show file tree
Hide file tree
Showing 20 changed files with 385 additions and 126 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@
* Removed `with_argparser_and_unknown_args` since it was deprecated in 1.3.0.
* Replaced `cmd2.Cmd.completion_header` with `cmd2.Cmd.formatted_completions`. See Enhancements
for description of this new class member.
* Settables now have new initialization parameters. It is now a required parameter to supply the reference to the
object that holds the settable attribute. `cmd2.Cmd.settables` is no longer a public dict attribute - it is now a
property that aggregates all Settables across all registered CommandSets.
* 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.
* Added `cmd2.exceptions.PassThroughException` to raise unhandled command exceptions instead of printing them.
* Added support for ANSI styles and newlines in tab completion results using `cmd2.Cmd.formatted_completions`.
`cmd2` provides this capability automatically if you return argparse completion matches as `CompletionItems`.

* Settables enhancements:
* Settables may be optionally scoped to a CommandSet. Settables added to CommandSets will appear when a
CommandSet is registered and disappear when a CommandSet is unregistered. Optionally, scoped Settables
may have a prepended prefix.
* Settables now allow changes to be applied to any arbitrary object attribute. It no longer needs to match an
attribute added to the cmd2 instance itself.
## 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
20 changes: 10 additions & 10 deletions cmd2/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def style_aware_write(fileobj: IO[str], msg: str) -> None:
fileobj.write(msg)


def fg_lookup(fg_name: Union[str, fg]) -> Fore:
def fg_lookup(fg_name: Union[str, fg]) -> str:
"""
Look up ANSI escape codes based on foreground color name.
Expand All @@ -239,16 +239,16 @@ def fg_lookup(fg_name: Union[str, fg]) -> Fore:
:raises: ValueError: if the color cannot be found
"""
if isinstance(fg_name, fg):
return fg_name.value
return str(fg_name.value)

try:
ansi_escape = fg[fg_name.lower()].value
except KeyError:
raise ValueError('Foreground color {!r} does not exist; must be one of: {}'.format(fg_name, fg.colors()))
return ansi_escape
return str(ansi_escape)


def bg_lookup(bg_name: Union[str, bg]) -> Back:
def bg_lookup(bg_name: Union[str, bg]) -> str:
"""
Look up ANSI escape codes based on background color name.
Expand All @@ -257,13 +257,13 @@ def bg_lookup(bg_name: Union[str, bg]) -> Back:
:raises: ValueError: if the color cannot be found
"""
if isinstance(bg_name, bg):
return bg_name.value
return str(bg_name.value)

try:
ansi_escape = bg[bg_name.lower()].value
except KeyError:
raise ValueError('Background color {!r} does not exist; must be one of: {}'.format(bg_name, bg.colors()))
return ansi_escape
return str(ansi_escape)


# noinspection PyShadowingNames
Expand Down Expand Up @@ -292,13 +292,13 @@ def style(
:return: the stylized string
"""
# List of strings that add style
additions = []
additions: List[str] = []

# List of strings that remove style
removals = []
removals: List[str] = []

# Convert the text object into a string if it isn't already one
text = "{}".format(text)
text_formatted = "{}".format(text)

# Process the style settings
if fg:
Expand All @@ -322,7 +322,7 @@ def style(
removals.append(UNDERLINE_DISABLE)

# Combine the ANSI style sequences with the text
return cast(str, "".join(additions) + text + "".join(removals))
return "".join(additions) + text_formatted + "".join(removals)


# Default styles for printing strings of various types.
Expand Down
141 changes: 110 additions & 31 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,15 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
gettext,
)
from typing import (
IO,
Any,
Callable,
Dict,
Iterable,
List,
NoReturn,
Optional,
Sequence,
Tuple,
Type,
Union,
Expand All @@ -220,6 +223,15 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
constants,
)

try:
from typing import (
Protocol,
)
except ImportError:
from typing_extensions import ( # type: ignore[misc]
Protocol,
)

############################################################################################################
# The following are names of custom argparse argument attributes added by cmd2
############################################################################################################
Expand Down Expand Up @@ -286,6 +298,59 @@ def __init__(self, value: object, desc: str = '', *args: Any) -> None:
############################################################################################################
# Class and functions related to ChoicesCallable
############################################################################################################


class ChoicesProviderFunc(Protocol):
"""
Function that returns a list of choices in support of tab completion
"""

def __call__(self) -> List[str]:
... # pragma: no cover


class ChoicesProviderFuncWithTokens(Protocol):
"""
Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.
"""

def __call__(self, *, arg_tokens: Dict[str, List[str]]) -> List[str]:
... # pragma: no cover


class CompleterFunc(Protocol):
"""
Function to support tab completion with the provided state of the user prompt
"""

def __call__(
self,
text: str,
line: str,
begidx: int,
endidx: int,
) -> List[str]:
... # pragma: no cover


class CompleterFuncWithTokens(Protocol):
"""
Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior
arguments.
"""

def __call__(
self,
text: str,
line: str,
begidx: int,
endidx: int,
*,
arg_tokens: Dict[str, List[str]],
) -> List[str]:
... # pragma: no cover


class ChoicesCallable:
"""
Enables using a callable as the choices provider for an argparse argument.
Expand All @@ -295,7 +360,7 @@ class ChoicesCallable:
def __init__(
self,
is_completer: bool,
to_call: Union[Callable[[], List[str]], Callable[[str, str, int, int], List[str]]],
to_call: Union[CompleterFunc, CompleterFuncWithTokens, ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
) -> None:
"""
Initializer
Expand Down Expand Up @@ -328,12 +393,18 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall
setattr(action, ATTR_CHOICES_CALLABLE, choices_callable)


def set_choices_provider(action: argparse.Action, choices_provider: Callable[[], List[str]]) -> None:
def set_choices_provider(
action: argparse.Action,
choices_provider: Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
) -> None:
"""Set choices_provider on an argparse action"""
_set_choices_callable(action, ChoicesCallable(is_completer=False, to_call=choices_provider))


def set_completer(action: argparse.Action, completer: Callable[[str, str, int, int], List[str]]) -> None:
def set_completer(
action: argparse.Action,
completer: Union[CompleterFunc, CompleterFuncWithTokens],
) -> None:
"""Set completer on an argparse action"""
_set_choices_callable(action, ChoicesCallable(is_completer=True, to_call=completer))

Expand All @@ -351,11 +422,11 @@ def _add_argument_wrapper(
self: argparse._ActionsContainer,
*args: Any,
nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None,
choices_provider: Optional[Callable[[], List[str]]] = None,
completer: Optional[Callable[[str, str, int, int], List[str]]] = None,
choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
suppress_tab_hint: bool = False,
descriptive_header: Optional[str] = None,
**kwargs: Any
**kwargs: Any,
) -> argparse.Action:
"""
Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2
Expand Down Expand Up @@ -646,9 +717,9 @@ def _format_usage(

# helper for wrapping lines
# noinspection PyMissingOrEmptyDocstring,PyShadowingNames
def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None):
lines = []
line = []
def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> List[str]:
lines: List[str] = []
line: List[str] = []
if prefix is not None:
line_len = len(prefix) - 1
else:
Expand Down Expand Up @@ -703,14 +774,14 @@ def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None):
# prefix with 'Usage:'
return '%s%s\n\n' % (prefix, usage)

def _format_action_invocation(self, action) -> str:
def _format_action_invocation(self, action: argparse.Action) -> str:
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
(metavar,) = self._metavar_formatter(action, default)(1)
return metavar

else:
parts = []
parts: List[str] = []

# if the Optional doesn't take a value, format is:
# -s, --long
Expand All @@ -729,7 +800,11 @@ def _format_action_invocation(self, action) -> str:
# End cmd2 customization

# noinspection PyMethodMayBeStatic
def _determine_metavar(self, action, default_metavar) -> Union[str, Tuple]:
def _determine_metavar(
self,
action: argparse.Action,
default_metavar: Union[str, Tuple[str, ...]],
) -> Union[str, Tuple[str, ...]]:
"""Custom method to determine what to use as the metavar value of an action"""
if action.metavar is not None:
result = action.metavar
Expand All @@ -742,11 +817,15 @@ def _determine_metavar(self, action, default_metavar) -> Union[str, Tuple]:
result = default_metavar
return result

def _metavar_formatter(self, action, default_metavar) -> Callable:
def _metavar_formatter(
self,
action: argparse.Action,
default_metavar: Union[str, Tuple[str, ...]],
) -> Callable[[int], Tuple[str, ...]]:
metavar = self._determine_metavar(action, default_metavar)

# noinspection PyMissingOrEmptyDocstring
def format(tuple_size):
def format(tuple_size: int) -> Tuple[str, ...]:
if isinstance(metavar, tuple):
return metavar
else:
Expand All @@ -755,7 +834,7 @@ def format(tuple_size):
return format

# noinspection PyProtectedMember
def _format_args(self, action, default_metavar) -> str:
def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tuple[str, ...]]) -> str:
"""Customized to handle ranged nargs and make other output less verbose"""
metavar = self._determine_metavar(action, default_metavar)
metavar_formatter = self._metavar_formatter(action, default_metavar)
Expand All @@ -780,7 +859,7 @@ def _format_args(self, action, default_metavar) -> str:
elif isinstance(action.nargs, int) and action.nargs > 1:
return '{}{{{}}}'.format('%s' % metavar_formatter(1), action.nargs)

return super()._format_args(action, default_metavar)
return super()._format_args(action, default_metavar) # type: ignore[arg-type]


# noinspection PyCompatibility
Expand All @@ -789,18 +868,18 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):

def __init__(
self,
prog=None,
usage=None,
description=None,
epilog=None,
parents=None,
formatter_class=Cmd2HelpFormatter,
prefix_chars='-',
fromfile_prefix_chars=None,
argument_default=None,
conflict_handler='error',
add_help=True,
allow_abbrev=True,
prog: Optional[str] = None,
usage: Optional[str] = None,
description: Optional[str] = None,
epilog: Optional[str] = None,
parents: Sequence[argparse.ArgumentParser] = [],
formatter_class: Type[argparse.HelpFormatter] = Cmd2HelpFormatter,
prefix_chars: str = '-',
fromfile_prefix_chars: Optional[str] = None,
argument_default: Optional[str] = None,
conflict_handler: str = 'error',
add_help: bool = True,
allow_abbrev: bool = True,
) -> None:
super(Cmd2ArgumentParser, self).__init__(
prog=prog,
Expand All @@ -817,7 +896,7 @@ def __init__(
allow_abbrev=allow_abbrev,
)

def add_subparsers(self, **kwargs):
def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction:
"""
Custom override. Sets a default title if one was not given.
Expand Down Expand Up @@ -895,7 +974,7 @@ def format_help(self) -> str:
# determine help from format above
return formatter.format_help() + '\n'

def _print_message(self, message, file=None):
def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None:
# Override _print_message to use style_aware_write() since we use ANSI escape characters to support color
if message:
if file is None:
Expand Down Expand Up @@ -923,7 +1002,7 @@ def set(self, new_val: Any) -> None:


# The default ArgumentParser class for a cmd2 app
DEFAULT_ARGUMENT_PARSER = Cmd2ArgumentParser
DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser


def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
Expand Down

0 comments on commit 8f981f3

Please sign in to comment.