diff options
author | Eric Lin <anselor@gmail.com> | 2021-03-15 15:18:29 -0400 |
---|---|---|
committer | anselor <anselor@gmail.com> | 2021-03-18 18:26:20 -0400 |
commit | 9d1b7c7f1068ce9b55ba160ebceeadd665d1bc02 (patch) | |
tree | 2add05f8f7f955f2ba6aafaa640afccafe8f514a | |
parent | f30627d5d2d0adc7db45aa26956372ea2cb3dc19 (diff) | |
download | cmd2-git-9d1b7c7f1068ce9b55ba160ebceeadd665d1bc02.tar.gz |
Some mypy validation fixes
-rw-r--r-- | cmd2/ansi.py | 21 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 70 | ||||
-rw-r--r-- | cmd2/clipboard.py | 8 | ||||
-rw-r--r-- | cmd2/exceptions.py | 7 | ||||
-rw-r--r-- | cmd2/plugin.py | 26 | ||||
-rw-r--r-- | cmd2/py_bridge.py | 12 | ||||
-rw-r--r-- | cmd2/utils.py | 85 | ||||
-rw-r--r-- | docs/api/utils.rst | 2 | ||||
-rw-r--r-- | tests_isolated/test_commandset/test_commandset.py | 2 |
9 files changed, 121 insertions, 112 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 0f34016d..741d3b8b 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -13,15 +13,16 @@ from typing import ( Any, List, Union, + cast, ) -import colorama +import colorama # type: ignore [import] from colorama import ( Back, Fore, Style, ) -from wcwidth import ( +from wcwidth import ( # type: ignore [import] wcswidth, ) @@ -86,14 +87,14 @@ class ColorBase(Enum): Support building a color string when self is the left operand e.g. fg.blue + "hello" """ - return str(self) + other + return cast(str, str(self) + other) def __radd__(self, other: Any) -> str: """ Support building a color string when self is the right operand e.g. "hello" + fg.reset """ - return other + str(self) + return cast(str, other + str(self)) @classmethod def colors(cls) -> List[str]: @@ -194,7 +195,7 @@ def style_aware_wcswidth(text: str) -> int: then this function returns -1. Replace tabs with spaces before calling this. """ # Strip ANSI style sequences since they cause wcswidth to return -1 - return wcswidth(strip_style(text)) + return cast(int, wcswidth(strip_style(text))) def widest_line(text: str) -> int: @@ -217,7 +218,7 @@ def widest_line(text: str) -> int: return max(lines_widths) -def style_aware_write(fileobj: IO, msg: str) -> None: +def style_aware_write(fileobj: IO[str], msg: str) -> None: """ Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting @@ -229,7 +230,7 @@ def style_aware_write(fileobj: IO, msg: str) -> None: fileobj.write(msg) -def fg_lookup(fg_name: Union[str, fg]) -> str: +def fg_lookup(fg_name: Union[str, fg]) -> Fore: """ Look up ANSI escape codes based on foreground color name. @@ -247,7 +248,7 @@ def fg_lookup(fg_name: Union[str, fg]) -> str: return ansi_escape -def bg_lookup(bg_name: Union[str, bg]) -> str: +def bg_lookup(bg_name: Union[str, bg]) -> Back: """ Look up ANSI escape codes based on background color name. @@ -321,7 +322,7 @@ def style( removals.append(UNDERLINE_DISABLE) # Combine the ANSI style sequences with the text - return "".join(additions) + text + "".join(removals) + return cast(str, "".join(additions) + text + "".join(removals)) # Default styles for printing strings of various types. @@ -400,4 +401,4 @@ def set_title_str(title: str) -> str: :param title: new title for the window :return: string to write to sys.stderr in order to set the window title to the desired test """ - return colorama.ansi.set_title(title) + return cast(str, colorama.ansi.set_title(title)) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2c71dec3..a9879d31 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -198,16 +198,21 @@ from argparse import ( ONE_OR_MORE, ZERO_OR_MORE, ArgumentError, - _, +) +from gettext import ( + gettext, ) from typing import ( Any, Callable, + Iterable, + List, NoReturn, Optional, Tuple, Type, Union, + cast, ) from . import ( @@ -261,11 +266,11 @@ class CompletionItem(str): See header of this file for more information """ - def __new__(cls, value: object, *args, **kwargs) -> str: - return super().__new__(cls, value) + def __new__(cls, value: object, *args: Any, **kwargs: Any) -> 'CompletionItem': + return cast(CompletionItem, super(CompletionItem, cls).__new__(cls, value)) # type: ignore [call-arg] # noinspection PyUnusedLocal - def __init__(self, value: object, desc: str = '', *args) -> None: + def __init__(self, value: object, desc: str = '', *args: Any) -> None: """ CompletionItem Initializer @@ -287,7 +292,11 @@ class ChoicesCallable: While argparse has the built-in choices attribute, it is limited to an iterable. """ - def __init__(self, is_completer: bool, to_call: Callable): + def __init__( + self, + is_completer: bool, + to_call: Union[Callable[[], List[str]], Callable[[str, str, int, int], List[str]]], + ) -> None: """ Initializer :param is_completer: True if to_call is a tab completion routine which expects @@ -319,12 +328,12 @@ 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) -> None: +def set_choices_provider(action: argparse.Action, choices_provider: Callable[[], List[str]]) -> 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) -> None: +def set_completer(action: argparse.Action, completer: Callable[[str, str, int, int], List[str]]) -> None: """Set completer on an argparse action""" _set_choices_callable(action, ChoicesCallable(is_completer=True, to_call=completer)) @@ -339,14 +348,14 @@ orig_actions_container_add_argument = argparse._ActionsContainer.add_argument def _add_argument_wrapper( - self, - *args, + self: argparse._ActionsContainer, + *args: Any, nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None, - choices_provider: Optional[Callable] = None, - completer: Optional[Callable] = None, + choices_provider: Optional[Callable[[], List[str]]] = None, + completer: Optional[Callable[[str, str, int, int], List[str]]] = None, suppress_tab_hint: bool = False, descriptive_header: Optional[str] = None, - **kwargs + **kwargs: Any ) -> argparse.Action: """ Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2 @@ -392,6 +401,7 @@ def _add_argument_wrapper( nargs_range = None if nargs is not None: + nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] # Check if nargs was given as a range if isinstance(nargs, tuple): @@ -402,11 +412,11 @@ def _add_argument_wrapper( # Validate nargs tuple if ( len(nargs) != 2 - or not isinstance(nargs[0], int) - or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) + or not isinstance(nargs[0], int) # type: ignore[unreachable] + or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) # type: ignore[misc] ): raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers') - if nargs[0] >= nargs[1]: + if nargs[0] >= nargs[1]: # type: ignore[misc] raise ValueError('Invalid nargs range. The first value must be less than the second') if nargs[0] < 0: raise ValueError('Negative numbers are invalid for nargs range') @@ -414,7 +424,7 @@ def _add_argument_wrapper( # Save the nargs tuple as our range setting nargs_range = nargs range_min = nargs_range[0] - range_max = nargs_range[1] + range_max = nargs_range[1] # type: ignore[misc] # Convert nargs into a format argparse recognizes if range_min == 0: @@ -460,7 +470,7 @@ def _add_argument_wrapper( # Overwrite _ActionsContainer.add_argument with our wrapper # noinspection PyProtectedMember -argparse._ActionsContainer.add_argument = _add_argument_wrapper +setattr(argparse._ActionsContainer, 'add_argument', _add_argument_wrapper) ############################################################################################################ # Patch ArgumentParser._get_nargs_pattern with our wrapper to nargs ranges @@ -472,7 +482,7 @@ orig_argument_parser_get_nargs_pattern = argparse.ArgumentParser._get_nargs_patt # noinspection PyProtectedMember -def _get_nargs_pattern_wrapper(self, action) -> str: +def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.Action) -> str: # Wrapper around ArgumentParser._get_nargs_pattern behavior to support nargs ranges nargs_range = getattr(action, ATTR_NARGS_RANGE, None) if nargs_range is not None: @@ -494,7 +504,7 @@ def _get_nargs_pattern_wrapper(self, action) -> str: # Overwrite ArgumentParser._get_nargs_pattern with our wrapper # noinspection PyProtectedMember -argparse.ArgumentParser._get_nargs_pattern = _get_nargs_pattern_wrapper +setattr(argparse.ArgumentParser, '_get_nargs_pattern', _get_nargs_pattern_wrapper) ############################################################################################################ @@ -505,7 +515,7 @@ orig_argument_parser_match_argument = argparse.ArgumentParser._match_argument # noinspection PyProtectedMember -def _match_argument_wrapper(self, action, arg_strings_pattern) -> int: +def _match_argument_wrapper(self: argparse.ArgumentParser, action: argparse.Action, arg_strings_pattern: str) -> int: # Wrapper around ArgumentParser._match_argument behavior to support nargs ranges nargs_pattern = self._get_nargs_pattern(action) match = re.match(nargs_pattern, arg_strings_pattern) @@ -521,7 +531,7 @@ def _match_argument_wrapper(self, action, arg_strings_pattern) -> int: # Overwrite ArgumentParser._match_argument with our wrapper # noinspection PyProtectedMember -argparse.ArgumentParser._match_argument = _match_argument_wrapper +setattr(argparse.ArgumentParser, '_match_argument', _match_argument_wrapper) ############################################################################################################ @@ -529,7 +539,7 @@ argparse.ArgumentParser._match_argument = _match_argument_wrapper ############################################################################################################ # noinspection PyPep8Naming -def _SubParsersAction_remove_parser(self, name: str): +def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: """ Removes a sub-parser from a sub-parsers group @@ -572,20 +582,26 @@ setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_pa class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): """Custom help formatter to configure ordering of help text""" - def _format_usage(self, usage, actions, groups, prefix) -> str: + def _format_usage( + self, + usage: Optional[str], + actions: Iterable[argparse.Action], + groups: Iterable[argparse._ArgumentGroup], + prefix: Optional[str] = None, + ) -> str: if prefix is None: - prefix = _('Usage: ') + prefix = gettext('Usage: ') # if usage is specified, use that if usage is not None: usage %= dict(prog=self._prog) # if no optionals or positionals are available, usage is just prog - elif usage is None and not actions: + elif not actions: usage = '%(prog)s' % dict(prog=self._prog) # if optionals and positionals are available, calculate usage - elif usage is None: + else: prog = '%(prog)s' % dict(prog=self._prog) # split optionals from positionals @@ -630,7 +646,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): # helper for wrapping lines # noinspection PyMissingOrEmptyDocstring,PyShadowingNames - def get_lines(parts, indent, prefix=None): + def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None): lines = [] line = [] if prefix is not None: diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 03931724..f31f5d5a 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -2,7 +2,11 @@ """ This module provides basic ability to copy from and paste to the clipboard/pastebuffer. """ -import pyperclip +from typing import ( + cast, +) + +import pyperclip # type: ignore [import] # noinspection PyProtectedMember from pyperclip import ( @@ -26,7 +30,7 @@ def get_paste_buffer() -> str: :return: contents of the clipboard """ - pb_str = pyperclip.paste() + pb_str = cast(str, pyperclip.paste()) return pb_str diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index b45167d1..011f53bd 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -1,6 +1,9 @@ # coding=utf-8 """Custom exceptions for cmd2""" +from typing import ( + Any, +) ############################################################################################################ # The following exceptions are part of the public API @@ -49,7 +52,7 @@ class CompletionError(Exception): - Tab completion hints """ - def __init__(self, *args, apply_style: bool = True): + def __init__(self, *args: Any, apply_style: bool = True) -> None: """ Initializer for CompletionError :param apply_style: If True, then ansi.style_error will be applied to the message text when printed. @@ -68,7 +71,7 @@ class PassThroughException(Exception): This class is used to wrap an exception that should be raised instead of printed. """ - def __init__(self, *args, wrapped_ex: BaseException): + def __init__(self, *args: Any, wrapped_ex: BaseException) -> None: """ Initializer for PassThroughException :param wrapped_ex: the exception that will be raised diff --git a/cmd2/plugin.py b/cmd2/plugin.py index e836b9d1..59169050 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -3,33 +3,37 @@ """Classes for the cmd2 plugin system""" import attr +from .parsing import ( + Statement, +) -@attr.s + +@attr.s(auto_attribs=True) class PostparsingData: """Data class containing information passed to postparsing hook methods""" - stop = attr.ib() - statement = attr.ib() + stop: bool + statement: Statement -@attr.s +@attr.s(auto_attribs=True) class PrecommandData: """Data class containing information passed to precommand hook methods""" - statement = attr.ib() + statement: Statement -@attr.s +@attr.s(auto_attribs=True) class PostcommandData: """Data class containing information passed to postcommand hook methods""" - stop = attr.ib() - statement = attr.ib() + stop: bool + statement: Statement -@attr.s +@attr.s(auto_attribs=True) class CommandFinalizationData: """Data class containing information passed to command finalization hook methods""" - stop = attr.ib() - statement = attr.ib() + stop: bool + statement: Statement diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index fd9b55fb..890363b0 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -10,16 +10,17 @@ from contextlib import ( redirect_stdout, ) from typing import ( + Any, + NamedTuple, Optional, ) -from .utils import ( +from .utils import ( # namedtuple_with_defaults, StdSim, - namedtuple_with_defaults, ) -class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr', 'stop', 'data'])): +class CommandResult(NamedTuple): """Encapsulates the results from a cmd2 app command :stdout: str - output captured from stdout while this command is executing @@ -56,6 +57,11 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr not for modification. """ + stdout: str = '' + stderr: str = '' + stop: bool = False + data: Any = None + def __bool__(self) -> bool: """Returns True if the command succeeded, otherwise False""" diff --git a/cmd2/utils.py b/cmd2/utils.py index 1008cb86..717d73b4 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -3,7 +3,6 @@ import argparse import collections -import collections.abc as collections_abc import functools import glob import inspect @@ -19,24 +18,27 @@ from enum import ( ) from typing import ( IO, + TYPE_CHECKING, Any, Callable, Dict, Iterable, List, - NamedTuple, Optional, TextIO, Type, - TYPE_CHECKING, + TypeVar, Union, + cast, ) from . import ( constants, ) + if TYPE_CHECKING: # pragma: no cover - import cmd2 + import cmd2 # noqa: F401 + def is_quoted(arg: str) -> bool: """ @@ -100,15 +102,15 @@ class Settable: def __init__( self, name: str, - val_type: Callable, + val_type: Union[Type[Any], Callable[[Any], Any]], description: str, *, settable_object: Optional[object] = None, settable_attrib_name: Optional[str] = None, - onchange_cb: Callable[[str, Any, Any], Any] = None, - choices: Iterable = None, - choices_provider: Optional[Callable] = None, - completer: Optional[Callable] = None + onchange_cb: Optional[Callable[[str, Any, Any], Any]] = None, + choices: Optional[Iterable[Any]] = None, + choices_provider: Optional[Callable[[], List[str]]] = None, + completer: Optional[Callable[[str, str, int, int], List[str]]] = None ): """ Settable Initializer @@ -174,36 +176,6 @@ class Settable: return new_value -def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], default_values: collections_abc.Iterable = ()): - """ - Convenience function for defining a namedtuple with default values - - From: https://stackoverflow.com/questions/11351032/namedtuple-and-default-values-for-optional-keyword-arguments - - Examples: - >>> Node = namedtuple_with_defaults('Node', 'val left right') - >>> Node() - Node(val=None, left=None, right=None) - >>> Node = namedtuple_with_defaults('Node', 'val left right', [1, 2, 3]) - >>> Node() - Node(val=1, left=2, right=3) - >>> Node = namedtuple_with_defaults('Node', 'val left right', {'right':7}) - >>> Node() - Node(val=None, left=None, right=7) - >>> Node(4) - Node(val=4, left=None, right=7) - """ - T: NamedTuple = collections.namedtuple(typename, field_names) - # noinspection PyProtectedMember,PyUnresolvedReferences - T.__new__.__defaults__ = (None,) * len(T._fields) - if isinstance(default_values, collections_abc.Mapping): - prototype = T(**default_values) - else: - prototype = T(*default_values) - T.__new__.__defaults__ = tuple(prototype) - return T - - def is_text_file(file_path: str) -> bool: """Returns if a file contains only ASCII or UTF-8 encoded text. @@ -241,13 +213,16 @@ def is_text_file(file_path: str) -> bool: return valid_text_file -def remove_duplicates(list_to_prune: List) -> List: +_T = TypeVar('_T') + + +def remove_duplicates(list_to_prune: List[_T]) -> List[_T]: """Removes duplicates from a list while preserving order of the items. :param list_to_prune: the list being pruned of duplicates :return: The pruned list """ - temp_dict = collections.OrderedDict() + temp_dict: collections.OrderedDict[_T, Any] = collections.OrderedDict() for item in list_to_prune: temp_dict[item] = None @@ -405,7 +380,7 @@ def find_editor() -> Optional[str]: return editor -def files_from_glob_pattern(pattern: str, access=os.F_OK) -> List[str]: +def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]: """Return a list of file paths based on a glob pattern. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -417,7 +392,7 @@ def files_from_glob_pattern(pattern: str, access=os.F_OK) -> List[str]: return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)] -def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]: +def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List[str]: """Return a list of file paths based on a list of glob patterns. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -472,7 +447,7 @@ class StdSim: Stores contents in internal buffer and optionally echos to the inner stream it is simulating. """ - def __init__(self, inner_stream, *, echo: bool = False, encoding: str = 'utf-8', errors: str = 'replace') -> None: + def __init__(self, inner_stream: TextIO, *, echo: bool = False, encoding: str = 'utf-8', errors: str = 'replace') -> None: """ StdSim Initializer :param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance. @@ -540,11 +515,11 @@ class StdSim: when running unit tests because pytest sets stdout to a pytest EncodedFile object. """ try: - return self.inner_stream.line_buffering + return bool(self.inner_stream.line_buffering) except AttributeError: return False - def __getattr__(self, item: str): + def __getattr__(self, item: str) -> Any: if item in self.__dict__: return self.__dict__[item] else: @@ -701,7 +676,7 @@ class ContextFlag: def __enter__(self) -> None: self.__count += 1 - def __exit__(self, *args) -> None: + def __exit__(self, *args: Any) -> None: self.__count -= 1 if self.__count < 0: raise ValueError("count has gone below 0") @@ -1060,7 +1035,7 @@ def get_styles_in_text(text: str) -> Dict[int, str]: return styles -def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None: +def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], category: str) -> None: """Categorize a function. The help command output will group the passed function under the @@ -1085,13 +1060,13 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None for item in func: setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) else: - if inspect.ismethod(func): - setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) + if inspect.ismethod(func) and hasattr(func, '__func__'): + setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined] else: setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) -def get_defining_class(meth: Callable) -> Optional[Type]: +def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]: """ Attempts to resolve the class that defined a method. @@ -1104,9 +1079,11 @@ def get_defining_class(meth: Callable) -> Optional[Type]: if isinstance(meth, functools.partial): return get_defining_class(meth.func) if inspect.ismethod(meth) or ( - inspect.isbuiltin(meth) and getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__') + inspect.isbuiltin(meth) + and getattr(meth, '__self__') is not None + and getattr(meth.__self__, '__class__') # type: ignore[attr-defined] ): - for cls in inspect.getmro(meth.__self__.__class__): + for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined] if meth.__name__ in cls.__dict__: return cls meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing @@ -1114,7 +1091,7 @@ def get_defining_class(meth: Callable) -> Optional[Type]: cls = getattr(inspect.getmodule(meth), meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0]) if isinstance(cls, type): return cls - return getattr(meth, '__objclass__', None) # handle special descriptor objects + return cast(type, getattr(meth, '__objclass__', None)) # handle special descriptor objects class CompletionMode(Enum): diff --git a/docs/api/utils.rst b/docs/api/utils.rst index 81c978c9..f9092d8d 100644 --- a/docs/api/utils.rst +++ b/docs/api/utils.rst @@ -90,8 +90,6 @@ Miscellaneous .. autofunction:: cmd2.utils.str_to_bool -.. autofunction:: cmd2.utils.namedtuple_with_defaults - .. autofunction:: cmd2.utils.categorize .. autofunction:: cmd2.utils.remove_duplicates diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 85608248..5bb68b08 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -20,10 +20,10 @@ from cmd2.exceptions import ( ) from .conftest import ( + WithCommandSets, complete_tester, normalize, run_cmd, - WithCommandSets, ) |