summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2021-04-03 18:41:26 -0400
committeranselor <anselor@gmail.com>2021-04-06 14:54:28 -0400
commit50d302913328931ecc9f61293892ac8f9aa61890 (patch)
treeb67f5586861cc57094f63efb27cf628476d7b544 /cmd2
parentd309493f29750433fd8bd6158bc194ed46fa0f7f (diff)
downloadcmd2-git-50d302913328931ecc9f61293892ac8f9aa61890.tar.gz
And that's the last of it. Passes mypy.
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/argparse_completer.py44
-rw-r--r--cmd2/argparse_custom.py34
-rw-r--r--cmd2/cmd2.py33
-rw-r--r--cmd2/decorators.py17
-rw-r--r--cmd2/history.py14
-rwxr-xr-xcmd2/parsing.py20
-rw-r--r--cmd2/py_bridge.py3
-rw-r--r--cmd2/table_creator.py22
-rw-r--r--cmd2/transcript.py4
-rw-r--r--cmd2/utils.py1
10 files changed, 118 insertions, 74 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 01a9e393..8a6fdea7 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -14,11 +14,11 @@ from collections import (
)
from typing import (
Any,
- cast,
Dict,
List,
Optional,
Union,
+ cast,
)
from . import (
@@ -32,6 +32,8 @@ from .argparse_custom import (
ATTR_NARGS_RANGE,
ATTR_SUPPRESS_TAB_HINT,
ChoicesCallable,
+ ChoicesProviderFuncBase,
+ ChoicesProviderFuncWithTokens,
CompletionItem,
generate_range_error,
)
@@ -317,9 +319,11 @@ class ArgparseCompleter:
# 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:
+ 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
@@ -332,9 +336,11 @@ class ArgparseCompleter:
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:
+ 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
@@ -438,9 +444,11 @@ class ArgparseCompleter:
# 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:
+ 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)
@@ -692,17 +700,23 @@ class ArgparseCompleter:
# 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.to_call(*args, **kwargs) # type: ignore[arg-type]
+ 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]
+ completion_items: List[str] = []
if isinstance(arg_choices, ChoicesCallable):
if not arg_choices.is_completer:
- completion_items = arg_choices.to_call(*args, **kwargs) # type: ignore[arg-type]
- else:
- completion_items = []
+ 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
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index c0e76e50..6b1c799d 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -214,7 +214,7 @@ from typing import (
Sequence,
Tuple,
Type,
- Union, runtime_checkable,
+ Union,
)
from . import (
@@ -225,10 +225,12 @@ from . import (
try:
from typing import (
Protocol,
+ runtime_checkable,
)
except ImportError:
from typing_extensions import ( # type: ignore[misc]
Protocol,
+ runtime_checkable,
)
############################################################################################################
@@ -359,6 +361,7 @@ class CompleterFuncWithTokens(Protocol):
CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
+
class ChoicesCallable:
"""
Enables using a callable as the choices provider for an argparse argument.
@@ -377,10 +380,35 @@ class ChoicesCallable:
:param to_call: the callable object that will be called to provide choices for the argument
"""
self.is_completer = is_completer
- if not isinstance(to_call, (CompleterFuncBase, CompleterFuncWithTokens)):
- raise ValueError('With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens')
+ if is_completer:
+ if not isinstance(to_call, (CompleterFuncBase, CompleterFuncWithTokens)): # pragma: no cover
+ # runtime checking of Protocols do not currently check the parameters of a function.
+ raise ValueError(
+ 'With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens'
+ )
+ else:
+ if not isinstance(to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)): # pragma: no cover
+ # runtime checking of Protocols do not currently check the parameters of a function.
+ raise ValueError(
+ 'With is_completer set to false, to_call must be either: '
+ 'ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens'
+ )
self.to_call = to_call
+ @property
+ def completer(self) -> CompleterFunc:
+ if not isinstance(self.to_call, (CompleterFuncBase, CompleterFuncWithTokens)): # pragma: no cover
+ # this should've been caught in the constructor, just a backup check
+ raise ValueError('Function is not a CompleterFunc')
+ return self.to_call
+
+ @property
+ def choices_provider(self) -> ChoicesProviderFunc:
+ if not isinstance(self.to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)): # pragma: no cover
+ # this should've been caught in the constructor, just a backup check
+ raise ValueError('Function is not a ChoicesProviderFunc')
+ return self.to_call
+
def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCallable) -> None:
"""
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 30140906..7e437270 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -80,7 +80,6 @@ from .argparse_custom import (
DEFAULT_ARGUMENT_PARSER,
ChoicesProviderFunc,
CompleterFunc,
- CompleterFuncWithTokens,
CompletionItem,
)
from .clipboard import (
@@ -3706,11 +3705,12 @@ class Cmd(cmd.Cmd):
| a list of tuples -> interpreted as (value, text), so
that the return value can differ from
the text advertised to the user"""
-
- local_opts = opts
+ local_opts: Union[List[str], List[Tuple[Any, Optional[str]]]]
if isinstance(opts, str):
- local_opts = list(zip(opts.split(), opts.split()))
- fulloptions = []
+ local_opts = cast(List[Tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split())))
+ else:
+ local_opts = opts
+ fulloptions: List[Tuple[Any, Optional[str]]] = []
for opt in local_opts:
if isinstance(opt, str):
fulloptions.append((opt, opt))
@@ -3739,7 +3739,7 @@ class Cmd(cmd.Cmd):
choice = int(response)
if choice < 1:
raise IndexError
- return fulloptions[choice - 1][0]
+ return str(fulloptions[choice - 1][0])
except (ValueError, IndexError):
self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(response, len(fulloptions)))
@@ -4192,18 +4192,16 @@ class Cmd(cmd.Cmd):
"""
# Detect whether IPython is installed
try:
- from IPython import (
+ import traitlets.config.loader as TraitletsLoader # type: ignore[import]
+ from IPython import ( # type: ignore[import]
start_ipython,
)
- from IPython.terminal.interactiveshell import (
+ from IPython.terminal.interactiveshell import ( # type: ignore[import]
TerminalInteractiveShell,
)
- from IPython.terminal.ipapp import (
+ from IPython.terminal.ipapp import ( # type: ignore[import]
TerminalIPythonApp,
)
- from traitlets.config.loader import (
- Config as TraitletsConfig,
- )
except ImportError:
self.perror("IPython package is not installed")
return None
@@ -4229,7 +4227,7 @@ class Cmd(cmd.Cmd):
local_vars['self'] = self
# Configure IPython
- config = TraitletsConfig()
+ config = TraitletsLoader.Config()
config.InteractiveShell.banner2 = (
'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n'
f'Run CLI commands with: {self.py_bridge_name}("command ...")\n'
@@ -5210,10 +5208,11 @@ class Cmd(cmd.Cmd):
self._validate_cmdfinalization_callable(func)
self._cmdfinalization_hooks.append(func)
- def _resolve_func_self(self,
- cmd_support_func: Callable[..., Any],
- cmd_self: Union[CommandSet, 'Cmd', None],
- ) -> Optional[object]:
+ def _resolve_func_self(
+ self,
+ cmd_support_func: Callable[..., Any],
+ cmd_self: Union[CommandSet, 'Cmd', None],
+ ) -> Optional[object]:
"""
Attempt to resolve a candidate instance to pass as 'self' for an unbound class method that was
used when defining command's argparse object. Since we restrict registration to only a single CommandSet
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index b62acaf8..d5a7a190 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -8,8 +8,9 @@ from typing import (
Dict,
List,
Optional,
+ Sequence,
Tuple,
- Union, Sequence, runtime_checkable, Protocol,
+ Union,
)
from . import (
@@ -106,7 +107,7 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) ->
"""
index = args.index(search_arg)
args_list = list(args)
- args_list[index: index + 1] = replace_arg
+ args_list[index : index + 1] = replace_arg
return args_list
@@ -134,11 +135,10 @@ ArgListCommandFunc = Union[ArgListCommandFuncOptionalBoolReturn, ArgListCommandF
def with_argument_list(
- func_arg: Optional[ArgListCommandFunc] = None, *, preserve_quotes: bool = False,
-) -> Union[
- RawCommandFuncOptionalBoolReturn,
- Callable[[ArgListCommandFunc], RawCommandFuncOptionalBoolReturn]
-]:
+ func_arg: Optional[ArgListCommandFunc] = None,
+ *,
+ preserve_quotes: bool = False,
+) -> Union[RawCommandFuncOptionalBoolReturn, Callable[[ArgListCommandFunc], RawCommandFuncOptionalBoolReturn]]:
"""
A decorator to alter the arguments passed to a ``do_*`` method. Default
passes a string of whatever the user typed. With this decorator, the
@@ -166,6 +166,7 @@ def with_argument_list(
:param func: The defined argument list command function
:return: Function that takes raw input and converts to an argument list to pass to the wrapped function.
"""
+
@functools.wraps(func)
def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]:
"""
@@ -182,7 +183,7 @@ def with_argument_list(
args_list = _arg_swap(args, statement, parsed_arglist)
return func(*args_list, **kwargs) # type: ignore[call-arg]
- command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
+ command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
cmd_wrapper.__doc__ = func.__doc__
return cmd_wrapper
diff --git a/cmd2/history.py b/cmd2/history.py
index 080ac594..c072d2e0 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -9,8 +9,11 @@ from collections import (
)
from typing import (
Callable,
+ Iterable,
+ List,
Optional,
- Union, List, Iterable, overload,
+ Union,
+ overload,
)
import attr
@@ -124,11 +127,11 @@ class History(List[HistoryItem]):
@overload
def append(self, new: HistoryItem) -> None:
- ...
+ ... # pragma: no cover
@overload
def append(self, new: Statement) -> None:
- ...
+ ... # pragma: no cover
def append(self, new: Union[Statement, HistoryItem]) -> None:
"""Append a new statement to the end of the History list.
@@ -136,10 +139,7 @@ class History(List[HistoryItem]):
:param new: Statement object which will be composed into a HistoryItem
and added to the end of the list
"""
- if isinstance(new, Statement):
- history_item = HistoryItem(new)
- else:
- history_item = new
+ history_item = HistoryItem(new) if isinstance(new, Statement) else new
super(History, self).append(history_item)
def clear(self) -> None:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index f97b0c64..46d32549 100755
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -5,6 +5,7 @@
import re
import shlex
from typing import (
+ Any,
Dict,
Iterable,
List,
@@ -86,7 +87,7 @@ class Macro:
@attr.s(auto_attribs=True, frozen=True)
-class Statement(str):
+class Statement(str): # type: ignore[override]
"""String subclass with additional attributes to store the results of parsing.
The ``cmd`` module in the standard library passes commands around as a
@@ -146,7 +147,7 @@ class Statement(str):
# if output was redirected, the destination file token (quotes preserved)
output_to: str = attr.ib(default='', validator=attr.validators.instance_of(str))
- def __new__(cls, value: object, *pos_args, **kw_args):
+ def __new__(cls, value: object, *pos_args: Any, **kw_args: Any) -> 'Statement':
"""Create a new instance of Statement.
We must override __new__ because we are subclassing `str` which is
@@ -241,18 +242,13 @@ class StatementParser:
:param aliases: dictionary containing aliases
:param shortcuts: dictionary containing shortcuts
"""
+ self.terminators: Tuple[str, ...]
if terminators is None:
self.terminators = (constants.MULTILINE_TERMINATOR,)
else:
self.terminators = tuple(terminators)
- if multiline_commands is None:
- self.multiline_commands = tuple()
- else:
- self.multiline_commands = tuple(multiline_commands)
- if aliases is None:
- self.aliases = dict()
- else:
- self.aliases = aliases
+ self.multiline_commands: Tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else ()
+ self.aliases: Dict[str, str] = aliases if aliases is not None else {}
if shortcuts is None:
shortcuts = constants.DEFAULT_SHORTCUTS
@@ -315,7 +311,7 @@ class StatementParser:
valid = False
if not isinstance(word, str):
- return False, 'must be a string. Received {} instead'.format(str(type(word)))
+ return False, 'must be a string. Received {} instead'.format(str(type(word))) # type: ignore[unreachable]
if not word:
return False, 'cannot be an empty string'
@@ -670,7 +666,7 @@ class StatementParser:
:param tokens: the tokens as parsed by shlex
:return: a new list of tokens, further split using punctuation
"""
- punctuation = []
+ punctuation: List[str] = []
punctuation.extend(self.terminators)
punctuation.extend(constants.REDIRECTION_CHARS)
diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py
index a54703fd..6fd00e07 100644
--- a/cmd2/py_bridge.py
+++ b/cmd2/py_bridge.py
@@ -17,7 +17,8 @@ from typing import (
NamedTuple,
Optional,
TextIO,
- cast, Union,
+ Union,
+ cast,
)
from .utils import ( # namedtuple_with_defaults,
diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py
index d9377bc4..80874086 100644
--- a/cmd2/table_creator.py
+++ b/cmd2/table_creator.py
@@ -23,7 +23,7 @@ from typing import (
Union,
)
-from wcwidth import (
+from wcwidth import ( # type: ignore[import]
wcwidth,
)
@@ -89,7 +89,7 @@ class Column:
if width is not None and width < 1:
raise ValueError("Column width cannot be less than 1")
else:
- self.width = width
+ self.width: int = width if width is not None else -1
self.header_horiz_align = header_horiz_align
self.header_vert_align = header_vert_align
@@ -138,7 +138,7 @@ class TableCreator:
# For headers with the width not yet set, use the width of the
# widest line in the header or 1 if the header has no width
- if col.width is None:
+ if col.width < 0:
col.width = max(1, ansi.widest_line(col.header))
@staticmethod
@@ -218,7 +218,11 @@ class TableCreator:
:return: wrapped text
"""
- def add_word(word_to_add: str, is_last_word: bool):
+ # MyPy Issue #7057 documents regression requiring nonlocals to be defined earlier
+ cur_line_width = 0
+ total_lines = 0
+
+ def add_word(word_to_add: str, is_last_word: bool) -> None:
"""
Called from loop to add a word to the wrapped text
@@ -431,7 +435,7 @@ class TableCreator:
def __init__(self) -> None:
# Data in this cell split into individual lines
- self.lines = []
+ self.lines: Deque[str] = deque()
# Display width of this cell
self.width = 0
@@ -705,7 +709,7 @@ class BorderedTable(TableCreator):
data_width = sum(col.width for col in self.cols)
return base_width + data_width
- def generate_table_top_border(self):
+ def generate_table_top_border(self) -> str:
"""Generate a border which appears at the top of the header and data section"""
pre_line = '╔' + self.padding * '═'
@@ -720,7 +724,7 @@ class BorderedTable(TableCreator):
row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
)
- def generate_header_bottom_border(self):
+ def generate_header_bottom_border(self) -> str:
"""Generate a border which appears at the bottom of the header"""
pre_line = '╠' + self.padding * '═'
@@ -735,7 +739,7 @@ class BorderedTable(TableCreator):
row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
)
- def generate_row_bottom_border(self):
+ def generate_row_bottom_border(self) -> str:
"""Generate a border which appears at the bottom of rows"""
pre_line = '╟' + self.padding * '─'
@@ -750,7 +754,7 @@ class BorderedTable(TableCreator):
row_data=self.empty_data, fill_char='─', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
)
- def generate_table_bottom_border(self):
+ def generate_table_bottom_border(self) -> str:
"""Generate a border which appears at the bottom of the table"""
pre_line = '╚' + self.padding * '═'
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index eec627c8..7ea5b8a9 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -13,10 +13,12 @@ import re
import unittest
from typing import (
TYPE_CHECKING,
+ Iterator,
List,
Optional,
+ TextIO,
Tuple,
- cast, Iterator, IO, TextIO,
+ cast,
)
from . import (
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 3a7dbc6e..4f3eae7b 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -36,7 +36,6 @@ from . import (
from .argparse_custom import (
ChoicesProviderFunc,
CompleterFunc,
- CompleterFuncWithTokens,
)
if TYPE_CHECKING: # pragma: no cover