diff options
author | xNinjaKittyx <xNinjaKittyx@users.noreply.github.com> | 2020-12-15 17:21:33 -0800 |
---|---|---|
committer | xNinjaKittyx <xNinjaKittyx@users.noreply.github.com> | 2020-12-15 18:20:13 -0800 |
commit | 9aa54a5b27468d61337528cb1e1b5b9b11a80978 (patch) | |
tree | 567693115cc101efb9254a96d96d80e9f9ccd557 /cmd2 | |
parent | 03c65c60b39e369958b056c5c844d36d515c8a63 (diff) | |
download | cmd2-git-ci_improvements.tar.gz |
Adds pre-commit config to run various lintersci_improvements
This ads black, isort, pyupgrade, and flake8 to pre-commit-config.yaml
There are also some small changes to travis.yml and tasks.py to reduce
some repeated configurations that should be consolidated into
setup.cfg. Most other changes are automated by the linter scripts.
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/__init__.py | 5 | ||||
-rw-r--r-- | cmd2/ansi.py | 18 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 90 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 93 | ||||
-rw-r--r-- | cmd2/clipboard.py | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 960 | ||||
-rw-r--r-- | cmd2/command_definition.py | 14 | ||||
-rw-r--r-- | cmd2/decorators.py | 67 | ||||
-rw-r--r-- | cmd2/exceptions.py | 9 | ||||
-rw-r--r-- | cmd2/history.py | 13 | ||||
-rwxr-xr-x | cmd2/parsing.py | 55 | ||||
-rw-r--r-- | cmd2/plugin.py | 4 | ||||
-rw-r--r-- | cmd2/py_bridge.py | 12 | ||||
-rw-r--r-- | cmd2/rl_utils.py | 21 | ||||
-rw-r--r-- | cmd2/table_creator.py | 91 | ||||
-rw-r--r-- | cmd2/transcript.py | 24 | ||||
-rw-r--r-- | cmd2/utils.py | 103 |
17 files changed, 951 insertions, 629 deletions
diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 81e80efe..1f122b87 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -20,9 +20,11 @@ from .argparse_custom import Cmd2ArgumentParser, Cmd2AttributeWrapper, Completio # Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER import argparse + cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None) if cmd2_parser_module is not None: import importlib + importlib.import_module(cmd2_parser_module) # Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER @@ -30,8 +32,7 @@ from .argparse_custom import DEFAULT_ARGUMENT_PARSER 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 .decorators import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category, as_subcommand_to from .exceptions import Cmd2ArgparseError, SkipPostcommandHooks, CommandSetRegistrationError from . import plugin from .parsing import Statement diff --git a/cmd2/ansi.py b/cmd2/ansi.py index f172b87f..9d7ce0ee 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -59,6 +59,7 @@ class ColorBase(Enum): value: anything that when cast to a string returns an ANSI sequence """ + def __str__(self) -> str: """ Return ANSI color sequence instead of enum name @@ -92,6 +93,7 @@ class ColorBase(Enum): # noinspection PyPep8Naming class fg(ColorBase): """Enum class for foreground colors""" + black = Fore.BLACK red = Fore.RED green = Fore.GREEN @@ -115,6 +117,7 @@ class fg(ColorBase): # noinspection PyPep8Naming class bg(ColorBase): """Enum class for background colors""" + black = Back.BLACK red = Back.RED green = Back.GREEN @@ -184,8 +187,7 @@ def style_aware_write(fileobj: IO, msg: str) -> None: :param fileobj: the file object being written to :param msg: the string being written """ - if allow_style.lower() == STYLE_NEVER.lower() or \ - (allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()): + if allow_style.lower() == STYLE_NEVER.lower() or (allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()): msg = strip_style(msg) fileobj.write(msg) @@ -227,8 +229,15 @@ def bg_lookup(bg_name: Union[str, bg]) -> str: # noinspection PyShadowingNames -def style(text: Any, *, fg: Union[str, fg] = '', bg: Union[str, bg] = '', bold: bool = False, - dim: bool = False, underline: bool = False) -> str: +def style( + text: Any, + *, + fg: Union[str, fg] = '', + bg: Union[str, bg] = '', + bold: bool = False, + dim: bool = False, + underline: bool = False +) -> str: """ Apply ANSI colors and/or styles to a string and return it. The styling is self contained which means that at the end of the string reset code(s) are issued @@ -302,6 +311,7 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off :return: the correct string so that the alert message appears to the user to be printed above the current line. """ from colorama import Cursor + # Split the prompt lines since it can contain newline characters. prompt_lines = prompt.splitlines() diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 117bfd50..1b2b6e62 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -86,12 +86,13 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: class _ArgumentState: """Keeps state of an argument being parsed""" + def __init__(self, arg_action: argparse.Action) -> None: self.action = arg_action self.min = None self.max = None self.count = 0 - self.is_remainder = (self.action.nargs == argparse.REMAINDER) + self.is_remainder = self.action.nargs == argparse.REMAINDER # Check if nargs is a range nargs_range = getattr(self.action, ATTR_NARGS_RANGE, None) @@ -124,10 +125,11 @@ class _UnfinishedFlagError(CompletionError): CompletionError which occurs when the user has not finished the current flag :param flag_arg_state: information about the unfinished flag action """ - error = "Error: argument {}: {} ({} entered)".\ - format(argparse._get_action_name(flag_arg_state.action), - generate_range_error(flag_arg_state.min, flag_arg_state.max), - flag_arg_state.count) + error = "Error: argument {}: {} ({} entered)".format( + argparse._get_action_name(flag_arg_state.action), + generate_range_error(flag_arg_state.min, flag_arg_state.max), + flag_arg_state.count, + ) super().__init__(error) @@ -146,8 +148,10 @@ class _NoResultsError(CompletionError): # noinspection PyProtectedMember class ArgparseCompleter: """Automatic command line tab completion based on argparse parameters""" - def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *, - parent_tokens: Optional[Dict[str, List[str]]] = None) -> None: + + def __init__( + self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *, parent_tokens: Optional[Dict[str, List[str]]] = None + ) -> None: """ Create an ArgparseCompleter @@ -164,10 +168,10 @@ class ArgparseCompleter: parent_tokens = dict() self._parent_tokens = parent_tokens - self._flags = [] # all flags in this command - self._flag_to_action = {} # maps flags to the argparse action object - self._positional_actions = [] # actions for positional arguments (by position index) - self._subcommand_action = None # this will be set if self._parser has subcommands + self._flags = [] # all flags in this command + self._flag_to_action = {} # maps flags to the argparse action object + self._positional_actions = [] # actions for positional arguments (by position index) + self._subcommand_action = None # this will be set if self._parser has subcommands # Start digging through the argparse structures. # _actions is the top level container of parameter definitions @@ -186,8 +190,9 @@ class ArgparseCompleter: 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_command( + self, tokens: List[str], text: str, line: str, begidx: int, endidx: int, *, cmd_set: Optional[CommandSet] = None + ) -> List[str]: """ Complete the command using the argparse metadata and provided argument dictionary :raises: CompletionError for various types of tab completion errors @@ -243,9 +248,9 @@ class ArgparseCompleter: if arg_action == completer_action: return - error = ("Error: argument {}: not allowed with argument {}". - format(argparse._get_action_name(arg_action), - argparse._get_action_name(completer_action))) + error = "Error: argument {}: not allowed with argument {}".format( + argparse._get_action_name(arg_action), argparse._get_action_name(completer_action) + ) raise CompletionError(error) # Mark that this action completed the group @@ -315,9 +320,7 @@ class ArgparseCompleter: if action is not None: update_mutex_groups(action) - if isinstance(action, (argparse._AppendAction, - argparse._AppendConstAction, - argparse._CountAction)): + if isinstance(action, (argparse._AppendAction, argparse._AppendConstAction, argparse._CountAction)): # Flags with action set to append, append_const, and count can be reused # Therefore don't erase any tokens already consumed for this flag consumed_arg_values.setdefault(action.dest, []) @@ -362,10 +365,12 @@ class ArgparseCompleter: 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_command( + tokens[token_index:], text, line, begidx, endidx, cmd_set=cmd_set + ) else: # Invalid subcommand entered, so no way to complete remaining tokens return [] @@ -409,9 +414,9 @@ class ArgparseCompleter: # 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_for_arg( + flag_arg_state, text, line, begidx, endidx, consumed_arg_values, cmd_set=cmd_set + ) # If we have results, then return them if completion_results: @@ -421,8 +426,11 @@ class ArgparseCompleter: return completion_results # Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag - elif flag_arg_state.count < flag_arg_state.min or \ - not _single_prefix_char(text, self._parser) or skip_remaining_flags: + elif ( + flag_arg_state.count < flag_arg_state.min + or not _single_prefix_char(text, self._parser) + or skip_remaining_flags + ): raise _NoResultsError(self._parser, flag_arg_state.action) # Otherwise check if we have a positional to complete @@ -433,9 +441,9 @@ class ArgparseCompleter: 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_for_arg( + pos_arg_state, text, line, begidx, endidx, consumed_arg_values, cmd_set=cmd_set + ) # If we have results, then return them if completion_results: @@ -491,8 +499,7 @@ class ArgparseCompleter: def _format_completions(self, arg_state: _ArgumentState, completions: List[Union[str, CompletionItem]]) -> List[str]: # Check if the results are CompletionItems and that there aren't too many to display - if 1 < len(completions) <= self._cmd2_app.max_completion_items and \ - isinstance(completions[0], CompletionItem): + if 1 < len(completions) <= self._cmd2_app.max_completion_items and isinstance(completions[0], CompletionItem): # If the user has not already sorted the CompletionItems, then sort them before appending the descriptions if not self._cmd2_app.matches_sorted: @@ -530,7 +537,7 @@ class ArgparseCompleter: initial_width = base_width + token_width + desc_width if initial_width < min_width: - desc_width += (min_width - initial_width) + desc_width += min_width - initial_width cols = list() cols.append(Column(destination.upper(), width=token_width)) @@ -583,10 +590,17 @@ class ArgparseCompleter: 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_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]: """ Tab completion routine for an argparse argument :return: list of completions diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index d773f851..bd9e4cfb 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -219,6 +219,7 @@ more details. import argparse import re import sys + # noinspection PyUnresolvedReferences,PyProtectedMember from argparse import ONE_OR_MORE, ZERO_OR_MORE, ArgumentError, _ from typing import Any, Callable, Optional, Tuple, Type, Union @@ -270,6 +271,7 @@ class CompletionItem(str): See header of this file for more information """ + def __new__(cls, value: object, *args, **kwargs) -> str: return super().__new__(cls, value) @@ -295,6 +297,7 @@ class ChoicesCallable: Enables using a callable as the choices provider for an argparse argument. While argparse has the built-in choices attribute, it is limited to an iterable. """ + def __init__(self, is_method: bool, is_completer: bool, to_call: Callable): """ Initializer @@ -317,12 +320,16 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall """ # Verify consistent use of parameters if action.choices is not None: - err_msg = ("None of the following parameters can be used alongside a choices parameter:\n" - "choices_function, choices_method, completer_function, completer_method") + err_msg = ( + "None of the following parameters can be used alongside a choices parameter:\n" + "choices_function, choices_method, completer_function, completer_method" + ) raise (TypeError(err_msg)) elif action.nargs == 0: - err_msg = ("None of the following parameters can be used on an action that takes no arguments:\n" - "choices_function, choices_method, completer_function, completer_method") + err_msg = ( + "None of the following parameters can be used on an action that takes no arguments:\n" + "choices_function, choices_method, completer_function, completer_method" + ) raise (TypeError(err_msg)) setattr(action, ATTR_CHOICES_CALLABLE, choices_callable) @@ -357,15 +364,18 @@ def set_completer_method(action: argparse.Action, completer_method: Callable) -> orig_actions_container_add_argument = argparse._ActionsContainer.add_argument -def _add_argument_wrapper(self, *args, - nargs: Union[int, str, Tuple[int], Tuple[int, int], None] = None, - choices_function: Optional[Callable] = None, - choices_method: Optional[Callable] = None, - completer_function: Optional[Callable] = None, - completer_method: Optional[Callable] = None, - suppress_tab_hint: bool = False, - descriptive_header: Optional[str] = None, - **kwargs) -> argparse.Action: +def _add_argument_wrapper( + self, + *args, + nargs: Union[int, str, Tuple[int], Tuple[int, int], None] = None, + choices_function: Optional[Callable] = None, + choices_method: Optional[Callable] = None, + completer_function: Optional[Callable] = None, + completer_method: Optional[Callable] = None, + suppress_tab_hint: bool = False, + descriptive_header: Optional[str] = None, + **kwargs +) -> argparse.Action: """ Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2 @@ -405,8 +415,10 @@ def _add_argument_wrapper(self, *args, num_params_set = len(choices_callables) - choices_callables.count(None) if num_params_set > 1: - err_msg = ("Only one of the following parameters may be used at a time:\n" - "choices_function, choices_method, completer_function, completer_method") + err_msg = ( + "Only one of the following parameters may be used at a time:\n" + "choices_function, choices_method, completer_function, completer_method" + ) raise (ValueError(err_msg)) # Pre-process special ranged nargs @@ -421,8 +433,11 @@ def _add_argument_wrapper(self, *args, nargs = (nargs[0], constants.INFINITY) # Validate nargs tuple - if len(nargs) != 2 or not isinstance(nargs[0], int) or \ - not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY): + if ( + len(nargs) != 2 + or not isinstance(nargs[0], int) + or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) + ): raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers') if nargs[0] >= nargs[1]: raise ValueError('Invalid nargs range. The first value must be less than the second') @@ -669,7 +684,7 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): if line: lines.append(indent + ' '.join(line)) if prefix is not None: - lines[0] = lines[0][len(indent):] + lines[0] = lines[0][len(indent) :] return lines # if prog is short, follow it with optionals or positionals @@ -707,12 +722,12 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): usage = '\n'.join(lines) # prefix with 'Usage:' - return '%s%s\n\n' % (prefix, usage) + return '{}{}\n\n'.format(prefix, usage) def _format_action_invocation(self, action) -> str: if not action.option_strings: default = self._get_default_metavar_for_positional(action) - metavar, = self._metavar_formatter(action, default)(1) + (metavar,) = self._metavar_formatter(action, default)(1) return metavar else: @@ -756,7 +771,8 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): if isinstance(metavar, tuple): return metavar else: - return (metavar, ) * tuple_size + return (metavar,) * tuple_size + return format # noinspection PyProtectedMember @@ -792,19 +808,21 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): class Cmd2ArgumentParser(argparse.ArgumentParser): """Custom ArgumentParser class that improves error and help output""" - 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) -> None: + 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, + ) -> None: super(Cmd2ArgumentParser, self).__init__( prog=prog, usage=usage, @@ -817,7 +835,8 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): argument_default=argument_default, conflict_handler=conflict_handler, add_help=add_help, - allow_abbrev=allow_abbrev) + allow_abbrev=allow_abbrev, + ) def add_subparsers(self, **kwargs): """ @@ -853,8 +872,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): formatter = self._get_formatter() # usage - formatter.add_usage(self.usage, self._actions, - self._mutually_exclusive_groups) + formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # description formatter.add_text(self.description) @@ -912,6 +930,7 @@ class Cmd2AttributeWrapper: This makes it easy to know which attributes in a Namespace are arguments from a parser and which were added by cmd2. """ + def __init__(self, attribute: Any): self.__attribute = attribute diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index deb2f5cc..f4d2885b 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -3,6 +3,7 @@ This module provides basic ability to copy from and paste to the clipboard/pastebuffer. """ import pyperclip + # noinspection PyProtectedMember from pyperclip import PyperclipException diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c8f5a9bd..ed22ce1a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -48,25 +48,25 @@ from .argparse_custom import DEFAULT_ARGUMENT_PARSER, CompletionItem from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .command_definition import CommandSet from .constants import CLASS_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX -from .decorators import with_argparser, as_subcommand_to +from .decorators import as_subcommand_to, with_argparser from .exceptions import ( - CommandSetRegistrationError, Cmd2ShlexError, + CommandSetRegistrationError, EmbeddedConsoleExit, EmptyStatement, RedirectionError, - SkipPostcommandHooks + SkipPostcommandHooks, ) from .history import History, HistoryItem from .parsing import Macro, MacroArg, Statement, StatementParser, shlex_split from .rl_utils import RlType, rl_get_point, rl_make_safe_prompt, rl_set_prompt, rl_type, rl_warning, vt100_support -from .utils import CompletionError, get_defining_class, Settable +from .utils import CompletionError, Settable, get_defining_class # Set up readline if rl_type == RlType.NONE: # pragma: no cover sys.stderr.write(ansi.style_warning(rl_warning)) else: - from .rl_utils import rl_force_redisplay, readline + from .rl_utils import readline, rl_force_redisplay # Used by rlcompleter in Python console loaded by py command orig_rl_delims = readline.get_completer_delims() @@ -81,6 +81,7 @@ else: # Get the readline lib so we can make changes to it import ctypes + from .rl_utils import readline_lib rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") @@ -127,24 +128,36 @@ class Cmd(cmd.Cmd): Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes. """ + DEFAULT_EDITOR = utils.find_editor() - INTERNAL_COMMAND_EPILOG = ("Notes:\n" - " This command is for internal use and is not intended to be called from the\n" - " command line.") + INTERNAL_COMMAND_EPILOG = ( + "Notes:\n" " This command is for internal use and is not intended to be called from the\n" " command line." + ) # Sorting keys for strings ALPHABETICAL_SORT_KEY = utils.norm_fold NATURAL_SORT_KEY = utils.natural_keys - def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *, - persistent_history_file: str = '', persistent_history_length: int = 1000, - startup_script: str = '', use_ipython: bool = False, - allow_cli_args: bool = True, transcript_files: Optional[List[str]] = None, - allow_redirection: bool = True, multiline_commands: Optional[List[str]] = None, - terminators: Optional[List[str]] = None, shortcuts: Optional[Dict[str, str]] = None, - command_sets: Optional[Iterable[CommandSet]] = None, - auto_load_commands: bool = True) -> None: + def __init__( + self, + completekey: str = 'tab', + stdin=None, + stdout=None, + *, + persistent_history_file: str = '', + persistent_history_length: int = 1000, + startup_script: str = '', + use_ipython: bool = False, + allow_cli_args: bool = True, + transcript_files: Optional[List[str]] = None, + allow_redirection: bool = True, + multiline_commands: Optional[List[str]] = None, + terminators: Optional[List[str]] = None, + shortcuts: Optional[Dict[str, str]] = None, + command_sets: Optional[Iterable[CommandSet]] = None, + auto_load_commands: bool = True + ) -> None: """An easy but powerful framework for writing line-oriented command interpreters. Extends Python's cmd package. @@ -257,9 +270,9 @@ class Cmd(cmd.Cmd): # True if running inside a Python script or interactive console, False otherwise self._in_py = False - self.statement_parser = StatementParser(terminators=terminators, - multiline_commands=multiline_commands, - shortcuts=shortcuts) + self.statement_parser = StatementParser( + terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts + ) # Stores results from the last command run to enable usage of results in a Python script or interactive console # Built-in commands don't make use of this. It is purely there for user-defined commands and convenience. @@ -311,8 +324,7 @@ class Cmd(cmd.Cmd): # Check for command line args if allow_cli_args: parser = argparse.ArgumentParser() - parser.add_argument('-t', '--test', action="store_true", - help='Test against transcript(s) in FILE (wildcards OK)') + parser.add_argument('-t', '--test', action="store_true", help='Test against transcript(s) in FILE (wildcards OK)') callopts, callargs = parser.parse_known_args() # If transcript testing was called for, use other arguments as transcript files @@ -435,8 +447,11 @@ class Cmd(cmd.Cmd): :param subclass_match: If True, return all sub-classes of provided type, otherwise only search for exact match :return: Matching CommandSets """ - return [cmdset for cmdset in self._installed_command_sets - if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type))] + return [ + cmdset + for cmdset in self._installed_command_sets + if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) + ] def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]: """ @@ -460,9 +475,11 @@ class Cmd(cmd.Cmd): load_commandset_by_type(subclasses) else: init_sig = inspect.signature(cmdset_type.__init__) - if not (cmdset_type in existing_commandset_types - or len(init_sig.parameters) != 1 - or 'self' not in init_sig.parameters): + if not ( + cmdset_type in existing_commandset_types + or len(init_sig.parameters) != 1 + or 'self' not in init_sig.parameters + ): cmdset = cmdset_type() self.register_command_set(cmdset) @@ -482,14 +499,16 @@ class Cmd(cmd.Cmd): methods = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) - and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX)) + and hasattr(meth, '__name__') + and meth.__name__.startswith(COMMAND_FUNC_PREFIX), + ) default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None) installed_attributes = [] try: for method_name, method in methods: - command = method_name[len(COMMAND_FUNC_PREFIX):] + command = method_name[len(COMMAND_FUNC_PREFIX) :] self._install_command_function(command, method, type(cmdset).__name__) installed_attributes.append(method_name) @@ -522,8 +541,7 @@ class Cmd(cmd.Cmd): if cmdset in self._installed_command_sets: self._installed_command_sets.remove(cmdset) if cmdset in self._cmd_to_command_sets.values(): - self._cmd_to_command_sets = \ - {key: val for key, val in self._cmd_to_command_sets.items() if val is not cmdset} + self._cmd_to_command_sets = {key: val for key, val in self._cmd_to_command_sets.items() if val is not cmdset} cmdset.on_unregistered() raise @@ -578,10 +596,12 @@ class Cmd(cmd.Cmd): methods = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) - and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX)) + and hasattr(meth, '__name__') + and meth.__name__.startswith(COMMAND_FUNC_PREFIX), + ) for method in methods: - cmd_name = method[0][len(COMMAND_FUNC_PREFIX):] + cmd_name = method[0][len(COMMAND_FUNC_PREFIX) :] # Enable the command before uninstalling it to make sure we remove both # the real functions and the ones used by the DisabledCommand object. @@ -605,10 +625,12 @@ class Cmd(cmd.Cmd): methods = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) - and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX)) + and hasattr(meth, '__name__') + and meth.__name__.startswith(COMMAND_FUNC_PREFIX), + ) for method in methods: - command_name = method[0][len(COMMAND_FUNC_PREFIX):] + command_name = method[0][len(COMMAND_FUNC_PREFIX) :] # Search for the base command function and verify it has an argparser defined if command_name in self.disabled_commands: @@ -625,9 +647,11 @@ class Cmd(cmd.Cmd): attached_cmdset = getattr(subparser, constants.PARSER_ATTR_COMMANDSET, None) if attached_cmdset is not None and attached_cmdset is not cmdset: raise CommandSetRegistrationError( - 'Cannot uninstall CommandSet when another CommandSet depends on it') + 'Cannot uninstall CommandSet when another CommandSet depends on it' + ) check_parser_uninstallable(subparser) break + if command_parser is not None: check_parser_uninstallable(command_parser) @@ -646,7 +670,7 @@ class Cmd(cmd.Cmd): predicate=lambda meth: isinstance(meth, Callable) and hasattr(meth, constants.SUBCMD_ATTR_NAME) and hasattr(meth, constants.SUBCMD_ATTR_COMMAND) - and hasattr(meth, constants.CMD_ATTR_ARGPARSER) + and hasattr(meth, constants.CMD_ATTR_ARGPARSER), ) # iterate through all matching methods @@ -670,12 +694,14 @@ class Cmd(cmd.Cmd): command_func = self.cmd_func(command_name) if command_func is None: - raise CommandSetRegistrationError('Could not find command "{}" needed by subcommand: {}' - .format(command_name, str(method))) + raise CommandSetRegistrationError( + 'Could not find command "{}" needed by subcommand: {}'.format(command_name, str(method)) + ) command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None) if command_parser is None: - raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}' - .format(command_name, str(method))) + raise CommandSetRegistrationError( + 'Could not find argparser for command "{}" needed by subcommand: {}'.format(command_name, str(method)) + ) def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser: if not subcmd_names: @@ -749,7 +775,7 @@ class Cmd(cmd.Cmd): predicate=lambda meth: isinstance(meth, Callable) and hasattr(meth, constants.SUBCMD_ATTR_NAME) and hasattr(meth, constants.SUBCMD_ATTR_COMMAND) - and hasattr(meth, constants.CMD_ATTR_ARGPARSER) + and hasattr(meth, constants.CMD_ATTR_ARGPARSER), ) # iterate through all matching methods @@ -766,14 +792,16 @@ class Cmd(cmd.Cmd): if command_func is None: # pragma: no cover # This really shouldn't be possible since _register_subcommands would prevent this from happening # but keeping in case it does for some strange reason - raise CommandSetRegistrationError('Could not find command "{}" needed by subcommand: {}' - .format(command_name, str(method))) + raise CommandSetRegistrationError( + 'Could not find command "{}" needed by subcommand: {}'.format(command_name, str(method)) + ) command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None) if command_parser is None: # pragma: no cover # This really shouldn't be possible since _register_subcommands would prevent this from happening # but keeping in case it does for some strange reason - raise CommandSetRegistrationError('Could not find argparser for command "{}" needed by subcommand: {}' - .format(command_name, str(method))) + raise CommandSetRegistrationError( + 'Could not find argparser for command "{}" needed by subcommand: {}'.format(command_name, str(method)) + ) for action in command_parser._actions: if isinstance(action, argparse._SubParsersAction): @@ -802,21 +830,26 @@ class Cmd(cmd.Cmd): def build_settables(self): """Create the dictionary of user-settable parameters""" - self.add_settable(Settable('allow_style', str, - 'Allow ANSI text style sequences in output (valid values: ' - '{}, {}, {})'.format(ansi.STYLE_TERMINAL, - ansi.STYLE_ALWAYS, - ansi.STYLE_NEVER), - choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER])) - - self.add_settable(Settable('always_show_hint', bool, - 'Display tab completion hint even when completion suggestions print')) + self.add_settable( + Settable( + 'allow_style', + str, + 'Allow ANSI text style sequences in output (valid values: ' + '{}, {}, {})'.format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER), + choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER], + ) + ) + + self.add_settable( + Settable('always_show_hint', bool, 'Display tab completion hint even when completion suggestions print') + ) self.add_settable(Settable('debug', bool, "Show full traceback on exception")) self.add_settable(Settable('echo', bool, "Echo command issued into output")) self.add_settable(Settable('editor', str, "Program used by 'edit'")) self.add_settable(Settable('feedback_to_output', bool, "Include nonessentials in '|', '>' results")) - self.add_settable(Settable('max_completion_items', int, - "Maximum number of CompletionItems to display during tab completion")) + self.add_settable( + Settable('max_completion_items', int, "Maximum number of CompletionItems to display during tab completion") + ) self.add_settable(Settable('quiet', bool, "Don't print nonessential feedback")) self.add_settable(Settable('timing', bool, "Report execution times")) @@ -834,8 +867,9 @@ class Cmd(cmd.Cmd): if new_val in [ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]: ansi.allow_style = new_val else: - raise ValueError("must be {}, {}, or {} (case-insensitive)".format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, - ansi.STYLE_NEVER)) + raise ValueError( + "must be {}, {}, or {} (case-insensitive)".format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER) + ) def _completion_supported(self) -> bool: """Return whether tab completion is supported""" @@ -910,6 +944,7 @@ class Cmd(cmd.Cmd): """ if self.debug and sys.exc_info() != (None, None, None): import traceback + traceback.print_exc() if isinstance(msg, Exception): @@ -1037,6 +1072,7 @@ class Cmd(cmd.Cmd): - Two empty lists """ import copy + unclosed_quote = '' quotes_to_try = copy.copy(constants.QUOTES) @@ -1083,8 +1119,9 @@ class Cmd(cmd.Cmd): return tokens, raw_tokens - def delimiter_complete(self, text: str, line: str, begidx: int, endidx: int, - match_against: Iterable, delimiter: str) -> List[str]: + def delimiter_complete( + self, text: str, line: str, begidx: int, endidx: int, match_against: Iterable, delimiter: str + ) -> List[str]: """ Performs tab completion against a list but each match is split on a delimiter and only the portion of the match being tab completed is shown as the completion suggestions. @@ -1144,9 +1181,16 @@ class Cmd(cmd.Cmd): return matches - def flag_based_complete(self, text: str, line: str, begidx: int, endidx: int, - flag_dict: Dict[str, Union[Iterable, Callable]], *, - all_else: Union[None, Iterable, Callable] = None) -> List[str]: + def flag_based_complete( + self, + text: str, + line: str, + begidx: int, + endidx: int, + flag_dict: Dict[str, Union[Iterable, Callable]], + *, + all_else: Union[None, Iterable, Callable] = None + ) -> List[str]: """Tab completes based on a particular flag preceding the token being completed. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1186,9 +1230,16 @@ class Cmd(cmd.Cmd): return completions_matches - def index_based_complete(self, text: str, line: str, begidx: int, endidx: int, - index_dict: Mapping[int, Union[Iterable, Callable]], *, - all_else: Union[None, Iterable, Callable] = None) -> List[str]: + def index_based_complete( + self, + text: str, + line: str, + begidx: int, + endidx: int, + index_dict: Mapping[int, Union[Iterable, Callable]], + *, + all_else: Union[None, Iterable, Callable] = None + ) -> List[str]: """Tab completes based on a fixed position in the input string. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1231,8 +1282,9 @@ class Cmd(cmd.Cmd): return matches # noinspection PyUnusedLocal - def path_complete(self, text: str, line: str, begidx: int, endidx: int, *, - path_filter: Optional[Callable[[str], bool]] = None) -> List[str]: + def path_complete( + self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None + ) -> List[str]: """Performs completion of local file system paths :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1375,8 +1427,7 @@ class Cmd(cmd.Cmd): return matches - def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, - complete_blank: bool = False) -> List[str]: + def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> List[str]: """Performs completion of executables either in a user's path or a given path :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1397,8 +1448,9 @@ class Cmd(cmd.Cmd): # Otherwise look for executables in the given path else: - return self.path_complete(text, line, begidx, endidx, - path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK)) + return self.path_complete( + text, line, begidx, endidx, path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK) + ) def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: Callable) -> List[str]: """Called by complete() as the first tab completion function for all commands @@ -1519,8 +1571,9 @@ class Cmd(cmd.Cmd): return metadata - def _display_matches_gnu_readline(self, substitution: str, matches: List[str], - longest_match_length: int) -> None: # pragma: no cover + def _display_matches_gnu_readline( + self, substitution: str, matches: List[str], longest_match_length: int + ) -> None: # pragma: no cover """Prints a match list using GNU readline's rl_display_match_list() This exists to print self.display_matches if it has data. Otherwise matches prints. @@ -1596,8 +1649,7 @@ class Cmd(cmd.Cmd): # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) - def _completion_for_command(self, text: str, line: str, begidx: int, - endidx: int, shortcut_to_restore: str) -> None: + def _completion_for_command(self, text: str, line: str, begidx: int, endidx: int, shortcut_to_restore: str) -> None: """ Helper function for complete() that performs command-specific tab completion @@ -1682,10 +1734,13 @@ class Cmd(cmd.Cmd): if func is not None and argparser is not None: import functools - compfunc = functools.partial(self._complete_argparse_command, - argparser=argparser, - preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES), - cmd_set=cmd_set) + + compfunc = functools.partial( + self._complete_argparse_command, + argparser=argparser, + preserve_quotes=getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES), + cmd_set=cmd_set, + ) else: compfunc = self.completedefault @@ -1712,6 +1767,7 @@ class Cmd(cmd.Cmd): # before we alter them. That way the suggestions will reflect how we parsed # the token being completed and not how readline did. import copy + self.display_matches = copy.copy(self.completion_matches) # Check if we need to add an opening quote @@ -1728,8 +1784,7 @@ class Cmd(cmd.Cmd): # For delimited matches, we check for a space in what appears before the display # matches (common_prefix) as well as in the display matches themselves. - if ' ' in common_prefix or (display_prefix - and any(' ' in match for match in self.display_matches)): + if ' ' in common_prefix or (display_prefix and any(' ' in match for match in self.display_matches)): add_quote = True # If there is a tab completion and any match has a space, then add an opening quote @@ -1808,7 +1863,7 @@ class Cmd(cmd.Cmd): shortcut_to_restore = shortcut # Adjust text and where it begins - text = text[len(shortcut_to_restore):] + text = text[len(shortcut_to_restore) :] begidx += len(shortcut_to_restore) break @@ -1852,12 +1907,20 @@ class Cmd(cmd.Cmd): rl_force_redisplay() return None - def _complete_argparse_command(self, text: str, line: str, begidx: int, endidx: int, *, - argparser: argparse.ArgumentParser, - preserve_quotes: bool, - cmd_set: Optional[CommandSet] = None) -> List[str]: + def _complete_argparse_command( + self, + text: str, + line: str, + begidx: int, + endidx: int, + *, + argparser: argparse.ArgumentParser, + preserve_quotes: bool, + cmd_set: Optional[CommandSet] = None + ) -> List[str]: """Completion function for argparse commands""" from .argparse_completer import ArgparseCompleter + completer = ArgparseCompleter(argparser, self) tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx) @@ -1885,13 +1948,19 @@ class Cmd(cmd.Cmd): def get_all_commands(self) -> List[str]: """Return a list of all commands""" - return [name[len(constants.COMMAND_FUNC_PREFIX):] for name in self.get_names() - if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))] + return [ + name[len(constants.COMMAND_FUNC_PREFIX) :] + for name in self.get_names() + if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name)) + ] def get_visible_commands(self) -> List[str]: """Return a list of commands that have not been hidden or disabled""" - return [command for command in self.get_all_commands() - if command not in self.hidden_commands and command not in self.disabled_commands] + return [ + command + for command in self.get_all_commands() + if command not in self.hidden_commands and command not in self.disabled_commands + ] def _get_alias_completion_items(self) -> List[CompletionItem]: """Return list of current alias names and values as CompletionItems""" @@ -1914,12 +1983,14 @@ class Cmd(cmd.Cmd): def get_help_topics(self) -> List[str]: """Return a list of help topics""" - all_topics = [name[len(constants.HELP_FUNC_PREFIX):] for name in self.get_names() - if name.startswith(constants.HELP_FUNC_PREFIX) and callable(getattr(self, name))] + all_topics = [ + name[len(constants.HELP_FUNC_PREFIX) :] + for name in self.get_names() + if name.startswith(constants.HELP_FUNC_PREFIX) and callable(getattr(self, name)) + ] # Filter out hidden and disabled commands - return [topic for topic in all_topics - if topic not in self.hidden_commands and topic not in self.disabled_commands] + return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands] # noinspection PyUnusedLocal def sigint_handler(self, signum: int, frame) -> None: @@ -2000,8 +2071,9 @@ class Cmd(cmd.Cmd): statement = self.statement_parser.parse_command_only(line) return statement.command, statement.args, statement.command_and_args - def onecmd_plus_hooks(self, line: str, *, add_to_history: bool = True, - raise_keyboard_interrupt: bool = False, py_bridge_call: bool = False) -> bool: + def onecmd_plus_hooks( + self, line: str, *, add_to_history: bool = True, raise_keyboard_interrupt: bool = False, py_bridge_call: bool = False + ) -> bool: """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks. :param line: command line to run @@ -2119,6 +2191,7 @@ class Cmd(cmd.Cmd): # Before the next command runs, fix any terminal problems like those # caused by certain binary characters having been printed to it. import subprocess + proc = subprocess.Popen(['stty', 'sane']) proc.communicate() @@ -2129,8 +2202,9 @@ class Cmd(cmd.Cmd): # modifications to the statement return data.stop - def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, add_to_history: bool = True, - stop_on_keyboard_interrupt: bool = True) -> bool: + def runcmds_plus_hooks( + self, cmds: List[Union[HistoryItem, str]], *, add_to_history: bool = True, stop_on_keyboard_interrupt: bool = True + ) -> bool: """ Used when commands are being run in an automated fashion like text scripts or history replays. The prompt and command line for each command will be printed if echo is True. @@ -2149,8 +2223,9 @@ class Cmd(cmd.Cmd): self.poutput('{}{}'.format(self.prompt, line)) try: - if self.onecmd_plus_hooks(line, add_to_history=add_to_history, - raise_keyboard_interrupt=stop_on_keyboard_interrupt): + if self.onecmd_plus_hooks( + line, add_to_history=add_to_history, raise_keyboard_interrupt=stop_on_keyboard_interrupt + ): return True except KeyboardInterrupt as e: if stop_on_keyboard_interrupt: @@ -2256,16 +2331,18 @@ class Cmd(cmd.Cmd): if orig_line != statement.raw: # Build a Statement that contains the resolved macro line # but the originally typed line for its raw member. - statement = Statement(statement.args, - raw=orig_line, - command=statement.command, - arg_list=statement.arg_list, - multiline_command=statement.multiline_command, - terminator=statement.terminator, - suffix=statement.suffix, - pipe_to=statement.pipe_to, - output=statement.output, - output_to=statement.output_to) + statement = Statement( + statement.args, + raw=orig_line, + command=statement.command, + arg_list=statement.arg_list, + multiline_command=statement.multiline_command, + terminator=statement.terminator, + suffix=statement.suffix, + pipe_to=statement.pipe_to, + output=statement.output, + output_to=statement.output_to, + ) return statement def _resolve_macro(self, statement: Statement) -> Optional[str]: @@ -2282,12 +2359,7 @@ class Cmd(cmd.Cmd): # Make sure enough arguments were passed in if len(statement.arg_list) < macro.minimum_arg_count: - self.perror( - "The macro '{}' expects at least {} argument(s)".format( - statement.command, - macro.minimum_arg_count - ) - ) + self.perror("The macro '{}' expects at least {} argument(s)".format(statement.command, macro.minimum_arg_count)) return None # Resolve the arguments in reverse and read their values from statement.argv since those @@ -2307,7 +2379,7 @@ class Cmd(cmd.Cmd): resolved = parts[0] + replacement + parts[1] # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved - for arg in statement.arg_list[macro.minimum_arg_count:]: + for arg in statement.arg_list[macro.minimum_arg_count :]: resolved += ' ' + arg # Restore any terminator, suffix, redirection, etc. @@ -2324,8 +2396,7 @@ class Cmd(cmd.Cmd): import subprocess # Initialize the redirection saved state - redir_saved_state = utils.RedirectionSavedState(self.stdout, sys.stdout, - self._cur_pipe_proc_reader, self._redirecting) + redir_saved_state = utils.RedirectionSavedState(self.stdout, sys.stdout, self._cur_pipe_proc_reader, self._redirecting) # The ProcReader for this command cmd_pipe_proc_reader = None # type: Optional[utils.ProcReader] @@ -2352,12 +2423,14 @@ class Cmd(cmd.Cmd): kwargs['start_new_session'] = True # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen(statement.pipe_to, - stdin=subproc_stdin, - stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, - shell=True, - **kwargs) + proc = subprocess.Popen( + statement.pipe_to, + stdin=subproc_stdin, + stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, + shell=True, + **kwargs + ) # Popen was called with shell=True so the user can chain pipe commands and redirect their output # like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process @@ -2372,8 +2445,7 @@ class Cmd(cmd.Cmd): if proc.returncode is not None: subproc_stdin.close() new_stdout.close() - raise RedirectionError( - 'Pipe process exited with code {} before command could run'.format(proc.returncode)) + raise RedirectionError('Pipe process exited with code {} before command could run'.format(proc.returncode)) else: redir_saved_state.redirecting = True cmd_pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr) @@ -2381,9 +2453,9 @@ class Cmd(cmd.Cmd): elif statement.output: import tempfile + if (not statement.output_to) and (not self._can_clip): - raise RedirectionError( - "Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies") + raise RedirectionError("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies") # Redirecting to a file elif statement.output_to: @@ -2487,8 +2559,11 @@ class Cmd(cmd.Cmd): func = self.cmd_func(statement.command) if func: # Check to see if this command should be stored in history - if statement.command not in self.exclude_from_history and \ - statement.command not in self.disabled_commands and add_to_history: + if ( + statement.command not in self.exclude_from_history + and statement.command not in self.disabled_commands + and add_to_history + ): self.history.append(statement) stop = func(statement) @@ -2712,11 +2787,8 @@ class Cmd(cmd.Cmd): ############################################################# # Top-level parser for alias - alias_description = ("Manage aliases\n" - "\n" - "An alias is a command that enables replacement of a word by another string.") - alias_epilog = ("See also:\n" - " macro") + alias_description = "Manage aliases\n" "\n" "An alias is a command that enables replacement of a word by another string." + alias_epilog = "See also:\n" " macro" alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog) alias_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') alias_subparsers.required = True @@ -2732,27 +2804,31 @@ class Cmd(cmd.Cmd): # alias -> create alias_create_description = "Create or overwrite an alias" - alias_create_epilog = ("Notes:\n" - " If you want to use redirection, pipes, or terminators in the value of the\n" - " alias, then quote them.\n" - "\n" - " Since aliases are resolved during parsing, tab completion will function as\n" - " it would for the actual command the alias resolves to.\n" - "\n" - "Examples:\n" - " alias create ls !ls -lF\n" - " alias create show_log !cat \"log file.txt\"\n" - " alias create save_results print_results \">\" out.txt\n") + alias_create_epilog = ( + "Notes:\n" + " If you want to use redirection, pipes, or terminators in the value of the\n" + " alias, then quote them.\n" + "\n" + " Since aliases are resolved during parsing, tab completion will function as\n" + " it would for the actual command the alias resolves to.\n" + "\n" + "Examples:\n" + " alias create ls !ls -lF\n" + " alias create show_log !cat \"log file.txt\"\n" + " alias create save_results print_results \">\" out.txt\n" + ) alias_create_parser = DEFAULT_ARGUMENT_PARSER(description=alias_create_description, epilog=alias_create_epilog) - alias_create_parser.add_argument('-s', '--silent', action='store_true', - help='do not print message confirming alias was created or\n' - 'overwritten') + alias_create_parser.add_argument( + '-s', '--silent', action='store_true', help='do not print message confirming alias was created or\n' 'overwritten' + ) alias_create_parser.add_argument('name', help='name of this alias') - alias_create_parser.add_argument('command', help='what the alias resolves to', - choices_method=_get_commands_aliases_and_macros_for_completion) - alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', - completer_method=path_complete) + alias_create_parser.add_argument( + 'command', help='what the alias resolves to', choices_method=_get_commands_aliases_and_macros_for_completion + ) + alias_create_parser.add_argument( + 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer_method=path_complete + ) @as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower()) def _alias_create(self, args: argparse.Namespace) -> None: @@ -2794,8 +2870,13 @@ class Cmd(cmd.Cmd): alias_delete_parser = DEFAULT_ARGUMENT_PARSER(description=alias_delete_description) alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases") - alias_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to delete', - choices_method=_get_alias_completion_items, descriptive_header='Value') + alias_delete_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='alias(es) to delete', + choices_method=_get_alias_completion_items, + descriptive_header='Value', + ) @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) def _alias_delete(self, args: argparse.Namespace) -> None: @@ -2815,18 +2896,29 @@ class Cmd(cmd.Cmd): # alias -> list alias_list_help = "list aliases" - alias_list_description = ("List specified aliases in a reusable form that can be saved to a startup\n" - "script to preserve aliases across sessions\n" - "\n" - "Without arguments, all aliases will be listed.") + alias_list_description = ( + "List specified aliases in a reusable form that can be saved to a startup\n" + "script to preserve aliases across sessions\n" + "\n" + "Without arguments, all aliases will be listed." + ) alias_list_parser = DEFAULT_ARGUMENT_PARSER(description=alias_list_description) - alias_list_parser.add_argument('-w', '--with_silent', action='store_true', - help="include --silent flag with listed aliases\n" - "Use this option when saving to a startup script that\n" - "should silently create aliases.") - alias_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='alias(es) to list', - choices_method=_get_alias_completion_items, descriptive_header='Value') + alias_list_parser.add_argument( + '-w', + '--with_silent', + action='store_true', + help="include --silent flag with listed aliases\n" + "Use this option when saving to a startup script that\n" + "should silently create aliases.", + ) + alias_list_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='alias(es) to list', + choices_method=_get_alias_completion_items, + descriptive_header='Value', + ) @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_delete_help) def _alias_list(self, args: argparse.Namespace) -> None: @@ -2869,11 +2961,8 @@ class Cmd(cmd.Cmd): ############################################################# # Top-level parser for macro - macro_description = ("Manage macros\n" - "\n" - "A macro is similar to an alias, but it can contain argument placeholders.") - macro_epilog = ("See also:\n" - " alias") + macro_description = "Manage macros\n" "\n" "A macro is similar to an alias, but it can contain argument placeholders." + macro_epilog = "See also:\n" " alias" macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog) macro_subparsers = macro_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') macro_subparsers.required = True @@ -2890,50 +2979,54 @@ class Cmd(cmd.Cmd): macro_create_help = "create or overwrite a macro" macro_create_description = "Create or overwrite a macro" - macro_create_epilog = ("A macro is similar to an alias, but it can contain argument placeholders.\n" - "Arguments are expressed when creating a macro using {#} notation where {1}\n" - "means the first argument.\n" - "\n" - "The following creates a macro called my_macro that expects two arguments:\n" - "\n" - " macro create my_macro make_dinner --meat {1} --veggie {2}\n" - "\n" - "When the macro is called, the provided arguments are resolved and the\n" - "assembled command is run. For example:\n" - "\n" - " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n" - "\n" - "Notes:\n" - " To use the literal string {1} in your command, escape it this way: {{1}}.\n" - "\n" - " Extra arguments passed to a macro are appended to resolved command.\n" - "\n" - " An argument number can be repeated in a macro. In the following example the\n" - " first argument will populate both {1} instances.\n" - "\n" - " macro create ft file_taxes -p {1} -q {2} -r {1}\n" - "\n" - " To quote an argument in the resolved command, quote it during creation.\n" - "\n" - " macro create backup !cp \"{1}\" \"{1}.orig\"\n" - "\n" - " If you want to use redirection, pipes, or terminators in the value of the\n" - " macro, then quote them.\n" - "\n" - " macro create show_results print_results -type {1} \"|\" less\n" - "\n" - " Because macros do not resolve until after hitting Enter, tab completion\n" - " will only complete paths while typing a macro.") + macro_create_epilog = ( + "A macro is similar to an alias, but it can contain argument placeholders.\n" + "Arguments are expressed when creating a macro using {#} notation where {1}\n" + "means the first argument.\n" + "\n" + "The following creates a macro called my_macro that expects two arguments:\n" + "\n" + " macro create my_macro make_dinner --meat {1} --veggie {2}\n" + "\n" + "When the macro is called, the provided arguments are resolved and the\n" + "assembled command is run. For example:\n" + "\n" + " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n" + "\n" + "Notes:\n" + " To use the literal string {1} in your command, escape it this way: {{1}}.\n" + "\n" + " Extra arguments passed to a macro are appended to resolved command.\n" + "\n" + " An argument number can be repeated in a macro. In the following example the\n" + " first argument will populate both {1} instances.\n" + "\n" + " macro create ft file_taxes -p {1} -q {2} -r {1}\n" + "\n" + " To quote an argument in the resolved command, quote it during creation.\n" + "\n" + " macro create backup !cp \"{1}\" \"{1}.orig\"\n" + "\n" + " If you want to use redirection, pipes, or terminators in the value of the\n" + " macro, then quote them.\n" + "\n" + " macro create show_results print_results -type {1} \"|\" less\n" + "\n" + " Because macros do not resolve until after hitting Enter, tab completion\n" + " will only complete paths while typing a macro." + ) macro_create_parser = DEFAULT_ARGUMENT_PARSER(description=macro_create_description, epilog=macro_create_epilog) - macro_create_parser.add_argument('-s', '--silent', action='store_true', - help='do not print message confirming macro was created or\n' - 'overwritten') + macro_create_parser.add_argument( + '-s', '--silent', action='store_true', help='do not print message confirming macro was created or\n' 'overwritten' + ) macro_create_parser.add_argument('name', help='name of this macro') - macro_create_parser.add_argument('command', help='what the macro resolves to', - choices_method=_get_commands_aliases_and_macros_for_completion) - macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER, - help='arguments to pass to command', completer_method=path_complete) + macro_create_parser.add_argument( + 'command', help='what the macro resolves to', choices_method=_get_commands_aliases_and_macros_for_completion + ) + macro_create_parser.add_argument( + 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer_method=path_complete + ) @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help) def _macro_create(self, args: argparse.Namespace) -> None: @@ -2973,7 +3066,7 @@ class Cmd(cmd.Cmd): cur_match = normal_matches.__next__() # Get the number string between the braces - cur_num_str = (re.findall(MacroArg.digit_pattern, cur_match.group())[0]) + cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0] cur_num = int(cur_num_str) if cur_num < 1: self.perror("Argument numbers must be greater than 0") @@ -2990,9 +3083,7 @@ class Cmd(cmd.Cmd): # Make sure the argument numbers are continuous if len(arg_nums) != max_arg_num: - self.perror( - "Not all numbers between 1 and {} are present " - "in the argument placeholders".format(max_arg_num)) + self.perror("Not all numbers between 1 and {} are present " "in the argument placeholders".format(max_arg_num)) return # Find all escaped arguments @@ -3021,8 +3112,13 @@ class Cmd(cmd.Cmd): macro_delete_description = "Delete specified macros or all macros if --all is used" macro_delete_parser = DEFAULT_ARGUMENT_PARSER(description=macro_delete_description) macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros") - macro_delete_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to delete', - choices_method=_get_macro_completion_items, descriptive_header='Value') + macro_delete_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='macro(s) to delete', + choices_method=_get_macro_completion_items, + descriptive_header='Value', + ) @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) def _macro_delete(self, args: argparse.Namespace) -> None: @@ -3042,18 +3138,29 @@ class Cmd(cmd.Cmd): # macro -> list macro_list_help = "list macros" - macro_list_description = ("List specified macros in a reusable form that can be saved to a startup script\n" - "to preserve macros across sessions\n" - "\n" - "Without arguments, all macros will be listed.") + macro_list_description = ( + "List specified macros in a reusable form that can be saved to a startup script\n" + "to preserve macros across sessions\n" + "\n" + "Without arguments, all macros will be listed." + ) macro_list_parser = DEFAULT_ARGUMENT_PARSER(description=macro_list_description) - macro_list_parser.add_argument('-w', '--with_silent', action='store_true', - help="include --silent flag with listed macros\n" - "Use this option when saving to a startup script that\n" - "should silently create macros.") - macro_list_parser.add_argument('names', nargs=argparse.ZERO_OR_MORE, help='macro(s) to list', - choices_method=_get_macro_completion_items, descriptive_header='Value') + macro_list_parser.add_argument( + '-w', + '--with_silent', + action='store_true', + help="include --silent flag with listed macros\n" + "Use this option when saving to a startup script that\n" + "should silently create macros.", + ) + macro_list_parser.add_argument( + 'names', + nargs=argparse.ZERO_OR_MORE, + help='macro(s) to list', + choices_method=_get_macro_completion_items, + descriptive_header='Value', + ) @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help) def _macro_list(self, args: argparse.Namespace) -> None: @@ -3100,8 +3207,9 @@ class Cmd(cmd.Cmd): strs_to_match = list(topics | visible_commands) return utils.basic_complete(text, line, begidx, endidx, strs_to_match) - def complete_help_subcommands(self, text: str, line: str, begidx: int, endidx: int, - arg_tokens: Dict[str, List[str]]) -> List[str]: + def complete_help_subcommands( + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] + ) -> List[str]: """Completes the subcommands argument of help""" # Make sure we have a command whose subcommands we will complete @@ -3119,17 +3227,25 @@ class Cmd(cmd.Cmd): tokens = [command] + arg_tokens['subcommands'] from .argparse_completer import ArgparseCompleter + completer = ArgparseCompleter(argparser, self) return completer.complete_subcommand_help(tokens, text, line, begidx, endidx) - help_parser = DEFAULT_ARGUMENT_PARSER(description="List available commands or provide " - "detailed help for a specific command") - help_parser.add_argument('-v', '--verbose', action='store_true', - help="print a list of all commands with descriptions of each") - help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for", - completer_method=complete_help_command) - help_parser.add_argument('subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for", - completer_method=complete_help_subcommands) + help_parser = DEFAULT_ARGUMENT_PARSER( + description="List available commands or provide " "detailed help for a specific command" + ) + help_parser.add_argument( + '-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each" + ) + help_parser.add_argument( + 'command', nargs=argparse.OPTIONAL, help="command to retrieve help for", completer_method=complete_help_command + ) + help_parser.add_argument( + 'subcommands', + nargs=argparse.REMAINDER, + help="subcommand(s) to retrieve help for", + completer_method=complete_help_subcommands, + ) # Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command if getattr(cmd.Cmd, 'complete_help', None) is not None: @@ -3150,6 +3266,7 @@ class Cmd(cmd.Cmd): # If the command function uses argparse, then use argparse's help if func is not None and argparser is not None: from .argparse_completer import ArgparseCompleter + completer = ArgparseCompleter(argparser, self) tokens = [args.command] + args.subcommands @@ -3290,9 +3407,7 @@ class Cmd(cmd.Cmd): break for doc_line in doc_block: - self.stdout.write('{: <{col_width}}{doc}\n'.format(command, - col_width=widest, - doc=doc_line)) + self.stdout.write('{: <{col_width}}{doc}\n'.format(command, col_width=widest, doc=doc_line)) command = '' self.stdout.write("\n") @@ -3322,8 +3437,7 @@ class Cmd(cmd.Cmd): # Return True to stop the command loop return True - def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], - prompt: str = 'Your choice? ') -> str: + def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> str: """Presents a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. @@ -3373,11 +3487,11 @@ class Cmd(cmd.Cmd): raise IndexError return 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))) + self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(response, len(fulloptions))) - def complete_set_value(self, text: str, line: str, begidx: int, endidx: int, - arg_tokens: Dict[str, List[str]]) -> List[str]: + def complete_set_value( + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] + ) -> List[str]: """Completes the value argument of set""" param = arg_tokens['param'][0] try: @@ -3391,14 +3505,19 @@ class Cmd(cmd.Cmd): # Settables with choices list the values of those choices instead of the arg name # in help text and this shows in tab completion hints. Set metavar to avoid this. arg_name = 'value' - settable_parser.add_argument(arg_name, metavar=arg_name, help=settable.description, - choices=settable.choices, - choices_function=settable.choices_function, - choices_method=settable.choices_method, - completer_function=settable.completer_function, - completer_method=settable.completer_method) + settable_parser.add_argument( + arg_name, + metavar=arg_name, + help=settable.description, + choices=settable.choices, + choices_function=settable.choices_function, + choices_method=settable.choices_method, + completer_function=settable.completer_function, + completer_method=settable.completer_method, + ) from .argparse_completer import ArgparseCompleter + completer = ArgparseCompleter(settable_parser, self) # Use raw_tokens since quotes have been preserved @@ -3407,19 +3526,32 @@ class Cmd(cmd.Cmd): # When tab completing value, we recreate the set command parser with a value argument specific to # the settable being edited. To make this easier, define a parent parser with all the common elements. - set_description = ("Set a settable parameter or show current settings of parameters\n" - "Call without arguments for a list of all settable parameters with their values.\n" - "Call with just param to view that parameter's value.") + set_description = ( + "Set a settable parameter or show current settings of parameters\n" + "Call without arguments for a list of all settable parameters with their values.\n" + "Call with just param to view that parameter's value." + ) set_parser_parent = DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False) - set_parser_parent.add_argument('-v', '--verbose', action='store_true', - help='include description of parameters when viewing') - set_parser_parent.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view', - choices_method=_get_settable_completion_items, descriptive_header='Description') + set_parser_parent.add_argument( + '-v', '--verbose', action='store_true', help='include description of parameters when viewing' + ) + set_parser_parent.add_argument( + 'param', + nargs=argparse.OPTIONAL, + help='parameter to set or view', + choices_method=_get_settable_completion_items, + descriptive_header='Description', + ) # Create the parser for the set command set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent]) - set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable', - completer_method=complete_set_value, suppress_tab_hint=True) + set_parser.add_argument( + 'value', + nargs=argparse.OPTIONAL, + help='new value for settable', + completer_method=complete_set_value, + suppress_tab_hint=True, + ) # Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value @with_argparser(set_parser, preserve_quotes=True) @@ -3474,15 +3606,15 @@ class Cmd(cmd.Cmd): for param in sorted(results, key=self.default_sort_key): result_str = results[param] if args.verbose: - self.poutput('{} # {}'.format(utils.align_left(result_str, width=max_len), - self.settables[param].description)) + self.poutput('{} # {}'.format(utils.align_left(result_str, width=max_len), self.settables[param].description)) else: self.poutput(result_str) shell_parser = DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt") shell_parser.add_argument('command', help='the command to run', completer_method=shell_cmd_complete) - shell_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', - completer_method=path_complete) + shell_parser.add_argument( + 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer_method=path_complete + ) # Preserve quotes since we are passing these strings to the shell @with_argparser(shell_parser, preserve_quotes=True) @@ -3501,10 +3633,12 @@ class Cmd(cmd.Cmd): # still receive the SIGINT since it is in the same process group as us. with self.sigint_protection: # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen(expanded_command, - stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, - stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, - shell=True) + proc = subprocess.Popen( + expanded_command, + stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, + stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, + shell=True, + ) proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr) proc_reader.wait() @@ -3560,8 +3694,7 @@ class Cmd(cmd.Cmd): # Set up tab completion for the Python console # rlcompleter relies on the default settings of the Python readline module if rl_type == RlType.GNU: - cmd2_env.readline_settings.basic_quotes = ctypes.cast(rl_basic_quote_characters, - ctypes.c_void_p).value + cmd2_env.readline_settings.basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value rl_basic_quote_characters.value = orig_rl_basic_quotes if 'gnureadline' in sys.modules: @@ -3639,14 +3772,16 @@ class Cmd(cmd.Cmd): else: sys.modules['readline'] = cmd2_env.readline_module - py_description = ("Invoke Python command or shell\n" - "\n" - "Note that, when invoking a command directly from the command line, this shell\n" - "has limited ability to parse Python statements into tokens. In particular,\n" - "there may be problems with whitespace and quotes depending on their placement.\n" - "\n" - "If you see strange parsing behavior, it's best to just open the Python shell\n" - "by providing no arguments to py and run more complex statements there.") + py_description = ( + "Invoke Python command or shell\n" + "\n" + "Note that, when invoking a command directly from the command line, this shell\n" + "has limited ability to parse Python statements into tokens. In particular,\n" + "there may be problems with whitespace and quotes depending on their placement.\n" + "\n" + "If you see strange parsing behavior, it's best to just open the Python shell\n" + "by providing no arguments to py and run more complex statements there." + ) py_parser = DEFAULT_ARGUMENT_PARSER(description=py_description) py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run") @@ -3670,6 +3805,7 @@ class Cmd(cmd.Cmd): raise EmbeddedConsoleExit from .py_bridge import PyBridge + py_bridge = PyBridge(self) saved_sys_path = None @@ -3740,9 +3876,10 @@ class Cmd(cmd.Cmd): # Otherwise we will open an interactive Python shell else: cprt = 'Type "help", "copyright", "credits" or "license" for more information.' - instructions = ('End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n' - 'Non-Python commands can be issued with: {}("your command")' - .format(self.py_bridge_name)) + instructions = ( + 'End with `Ctrl-D` (Unix) / `Ctrl-Z` (Windows), `quit()`, `exit()`.\n' + 'Non-Python commands can be issued with: {}("your command")'.format(self.py_bridge_name) + ) saved_cmd2_env = None @@ -3752,8 +3889,7 @@ class Cmd(cmd.Cmd): with self.sigint_protection: saved_cmd2_env = self._set_up_py_shell_env(interp) - interp.interact(banner="Python {} on {}\n{}\n\n{}\n". - format(sys.version, sys.platform, cprt, instructions)) + interp.interact(banner="Python {} on {}\n{}\n\n{}\n".format(sys.version, sys.platform, cprt, instructions)) except BaseException: # We don't care about any exception that happened in the interactive console pass @@ -3774,8 +3910,9 @@ class Cmd(cmd.Cmd): run_pyscript_parser = DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console") run_pyscript_parser.add_argument('script_path', help='path to the script file', completer_method=path_complete) - run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER, - help='arguments to pass to script', completer_method=path_complete) + run_pyscript_parser.add_argument( + 'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer_method=path_complete + ) @with_argparser(run_pyscript_parser) def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: @@ -3844,9 +3981,13 @@ class Cmd(cmd.Cmd): del py_bridge # Start ipy shell - embed(banner1=('Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n' - 'Run Python code from external files with: run filename.py\n'), - exit_msg='Leaving IPython, back to {}'.format(sys.argv[0])) + embed( + banner1=( + 'Entering an embedded IPython shell. Type quit or <Ctrl>-d to exit.\n' + 'Run Python code from external files with: run filename.py\n' + ), + exit_msg='Leaving IPython, back to {}'.format(sys.argv[0]), + ) if self.in_pyscript(): self.perror("Recursively entering interactive Python shells is not allowed") @@ -3865,35 +4006,50 @@ class Cmd(cmd.Cmd): history_parser = DEFAULT_ARGUMENT_PARSER(description=history_description) history_action_group = history_parser.add_mutually_exclusive_group() history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') - history_action_group.add_argument('-e', '--edit', action='store_true', - help='edit and then run selected history items') - history_action_group.add_argument('-o', '--output_file', metavar='FILE', - help='output commands to a script file, implies -s', - completer_method=path_complete) - history_action_group.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE', - help='output commands and results to a transcript file,\nimplies -s', - completer_method=path_complete) + history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items') + history_action_group.add_argument( + '-o', + '--output_file', + metavar='FILE', + help='output commands to a script file, implies -s', + completer_method=path_complete, + ) + history_action_group.add_argument( + '-t', + '--transcript', + metavar='TRANSCRIPT_FILE', + help='output commands and results to a transcript file,\nimplies -s', + completer_method=path_complete, + ) history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') history_format_group = history_parser.add_argument_group(title='formatting') - history_format_group.add_argument('-s', '--script', action='store_true', - help='output commands in script format, i.e. without command\n' - 'numbers') - history_format_group.add_argument('-x', '--expanded', action='store_true', - help='output fully parsed commands with any aliases and\n' - 'macros expanded, instead of typed commands') - history_format_group.add_argument('-v', '--verbose', action='store_true', - help='display history and include expanded commands if they\n' - 'differ from the typed command') - history_format_group.add_argument('-a', '--all', action='store_true', - help='display all commands, including ones persisted from\n' - 'previous sessions') - - history_arg_help = ("empty all history items\n" - "a one history item by number\n" - "a..b, a:b, a:, ..b items by indices (inclusive)\n" - "string items containing string\n" - "/regex/ items matching regular expression") + history_format_group.add_argument( + '-s', '--script', action='store_true', help='output commands in script format, i.e. without command\n' 'numbers' + ) + history_format_group.add_argument( + '-x', + '--expanded', + action='store_true', + help='output fully parsed commands with any aliases and\n' 'macros expanded, instead of typed commands', + ) + history_format_group.add_argument( + '-v', + '--verbose', + action='store_true', + help='display history and include expanded commands if they\n' 'differ from the typed command', + ) + history_format_group.add_argument( + '-a', '--all', action='store_true', help='display all commands, including ones persisted from\n' 'previous sessions' + ) + + history_arg_help = ( + "empty all history items\n" + "a one history item by number\n" + "a..b, a:b, a:, ..b items by indices (inclusive)\n" + "string items containing string\n" + "/regex/ items matching regular expression" + ) history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help) @with_argparser(history_parser) @@ -3906,15 +4062,13 @@ class Cmd(cmd.Cmd): # -v must be used alone with no other options if args.verbose: - if args.clear or args.edit or args.output_file or args.run or args.transcript \ - or args.expanded or args.script: + if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script: self.poutput("-v can not be used with any other options") self.poutput(self.history_parser.format_usage()) return # -s and -x can only be used if none of these options are present: [-c -r -e -o -t] - if (args.script or args.expanded) \ - and (args.clear or args.edit or args.output_file or args.run or args.transcript): + if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript): self.poutput("-s and -x can not be used with -c, -r, -e, -o, or -t") self.poutput(self.history_parser.format_usage()) return @@ -3966,6 +4120,7 @@ class Cmd(cmd.Cmd): return self.runcmds_plus_hooks(history) elif args.edit: import tempfile + fd, fname = tempfile.mkstemp(suffix='.txt', text=True) with os.fdopen(fd, 'w') as fobj: for command in history: @@ -4041,8 +4196,16 @@ class Cmd(cmd.Cmd): try: with open(hist_file, 'rb') as fobj: history = pickle.load(fobj) - except (AttributeError, EOFError, FileNotFoundError, ImportError, IndexError, KeyError, ValueError, - pickle.UnpicklingError): + except ( + AttributeError, + EOFError, + FileNotFoundError, + ImportError, + IndexError, + KeyError, + ValueError, + pickle.UnpicklingError, + ): # If any of these errors occur when attempting to unpickle, just use an empty history pass except OSError as ex: @@ -4070,6 +4233,7 @@ class Cmd(cmd.Cmd): # if the history file is in plain text format from 0.9.12 or lower # this will fail, and the history in the plain text file will be lost import atexit + atexit.register(self._persist_history) def _persist_history(self): @@ -4171,15 +4335,18 @@ class Cmd(cmd.Cmd): msg = '{} {} saved to transcript file {!r}' self.pfeedback(msg.format(commands_run, plural, transcript_file)) - edit_description = ("Run a text editor and optionally open a file with it\n" - "\n" - "The editor used is determined by a settable parameter. To set it:\n" - "\n" - " set editor (program-name)") + edit_description = ( + "Run a text editor and optionally open a file with it\n" + "\n" + "The editor used is determined by a settable parameter. To set it:\n" + "\n" + " set editor (program-name)" + ) edit_parser = DEFAULT_ARGUMENT_PARSER(description=edit_description) - edit_parser.add_argument('file_path', nargs=argparse.OPTIONAL, - help="optional path to a file to open in editor", completer_method=path_complete) + edit_parser.add_argument( + 'file_path', nargs=argparse.OPTIONAL, help="optional path to a file to open in editor", completer_method=path_complete + ) @with_argparser(edit_parser) def do_edit(self, args: argparse.Namespace) -> None: @@ -4211,18 +4378,24 @@ class Cmd(cmd.Cmd): else: return None - run_script_description = ("Run commands in script file that is encoded as either ASCII or UTF-8 text\n" - "\n" - "Script should contain one command per line, just like the command would be\n" - "typed in the console.\n" - "\n" - "If the -t/--transcript flag is used, this command instead records\n" - "the output of the script commands to a transcript for testing purposes.\n") + run_script_description = ( + "Run commands in script file that is encoded as either ASCII or UTF-8 text\n" + "\n" + "Script should contain one command per line, just like the command would be\n" + "typed in the console.\n" + "\n" + "If the -t/--transcript flag is used, this command instead records\n" + "the output of the script commands to a transcript for testing purposes.\n" + ) run_script_parser = DEFAULT_ARGUMENT_PARSER(description=run_script_description) - run_script_parser.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE', - help='record the output of the script as a transcript file', - completer_method=path_complete) + run_script_parser.add_argument( + '-t', + '--transcript', + metavar='TRANSCRIPT_FILE', + help='record the output of the script as a transcript file', + completer_method=path_complete, + ) run_script_parser.add_argument('script_path', help="path to the script file", completer_method=path_complete) @with_argparser(run_script_parser) @@ -4288,13 +4461,14 @@ class Cmd(cmd.Cmd): relative_run_script_description += ( "\n\n" "If this is called from within an already-running script, the filename will be\n" - "interpreted relative to the already-running script's directory.") + "interpreted relative to the already-running script's directory." + ) - relative_run_script_epilog = ("Notes:\n" - " This command is intended to only be used within text file scripts.") + relative_run_script_epilog = "Notes:\n" " This command is intended to only be used within text file scripts." - relative_run_script_parser = DEFAULT_ARGUMENT_PARSER(description=relative_run_script_description, - epilog=relative_run_script_epilog) + relative_run_script_parser = DEFAULT_ARGUMENT_PARSER( + description=relative_run_script_description, epilog=relative_run_script_epilog + ) relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_run_script_parser) @@ -4321,7 +4495,9 @@ class Cmd(cmd.Cmd): """ import time import unittest + import cmd2 + from .transcript import Cmd2TestCase class TestMyAppCase(Cmd2TestCase): @@ -4338,8 +4514,7 @@ class Cmd(cmd.Cmd): num_transcripts = len(transcripts_expanded) plural = '' if len(transcripts_expanded) == 1 else 's' self.poutput(ansi.style(utils.align_center(' cmd2 transcript test ', fill_char='='), bold=True)) - self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__, - rl_type)) + self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__, rl_type)) self.poutput('cwd: {}'.format(os.getcwd())) self.poutput('cmd2 app: {}'.format(sys.argv[0])) self.poutput(ansi.style('collected {} transcript{}'.format(num_transcripts, plural), bold=True)) @@ -4355,7 +4530,7 @@ class Cmd(cmd.Cmd): execution_time = time.time() - start_time if test_results.wasSuccessful(): ansi.style_aware_write(sys.stderr, stream.read()) - finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time) + finish_msg = ' {} transcript{} passed in {:.3f} seconds '.format(num_transcripts, plural, execution_time) finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='=')) self.poutput(finish_msg) else: @@ -4413,9 +4588,13 @@ class Cmd(cmd.Cmd): import shutil current_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt - terminal_str = ansi.async_alert_str(terminal_columns=shutil.get_terminal_size().columns, - prompt=current_prompt, line=readline.get_line_buffer(), - cursor_offset=rl_get_point(), alert_msg=alert_msg) + terminal_str = ansi.async_alert_str( + terminal_columns=shutil.get_terminal_size().columns, + prompt=current_prompt, + line=readline.get_line_buffer(), + cursor_offset=rl_get_point(), + alert_msg=alert_msg, + ) if rl_type == RlType.GNU: sys.stderr.write(terminal_str) sys.stderr.flush() @@ -4550,13 +4729,16 @@ class Cmd(cmd.Cmd): completer_func_name = constants.COMPLETER_FUNC_PREFIX + command # Add the disabled command record - self.disabled_commands[command] = DisabledCommand(command_function=command_function, - help_function=getattr(self, help_func_name, None), - completer_function=getattr(self, completer_func_name, None)) + self.disabled_commands[command] = DisabledCommand( + command_function=command_function, + help_function=getattr(self, help_func_name, None), + completer_function=getattr(self, completer_func_name, None), + ) # Overwrite the command and help functions to print the message - new_func = functools.partial(self._report_disabled_command_usage, - message_to_print=message_to_print.replace(constants.COMMAND_NAME, command)) + new_func = functools.partial( + self._report_disabled_command_usage, message_to_print=message_to_print.replace(constants.COMMAND_NAME, command) + ) setattr(self, self._cmd_func_name(command), new_func) setattr(self, help_func_name, new_func) @@ -4608,6 +4790,7 @@ class Cmd(cmd.Cmd): # Register a SIGINT signal handler for Ctrl+C import signal + original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) @@ -4669,11 +4852,7 @@ class Cmd(cmd.Cmd): # validate that the callable has the right number of parameters nparam = len(signature.parameters) if nparam != count: - raise TypeError('{} has {} positional arguments, expected {}'.format( - func.__name__, - nparam, - count, - )) + raise TypeError('{} has {} positional arguments, expected {}'.format(func.__name__, nparam, count,)) @classmethod def _validate_prepostloop_callable(cls, func: Callable[[None], None]) -> None: @@ -4682,9 +4861,7 @@ class Cmd(cmd.Cmd): # make sure there is no return notation signature = inspect.signature(func) if signature.return_annotation is not None: - raise TypeError("{} must declare return a return type of 'None'".format( - func.__name__, - )) + raise TypeError("{} must declare return a return type of 'None'".format(func.__name__,)) def register_preloop_hook(self, func: Callable[[None], None]) -> None: """Register a function to be called at the beginning of the command loop.""" @@ -4703,13 +4880,11 @@ class Cmd(cmd.Cmd): signature = inspect.signature(func) _, param = list(signature.parameters.items())[0] if param.annotation != plugin.PostparsingData: - raise TypeError("{} must have one parameter declared with type 'cmd2.plugin.PostparsingData'".format( - func.__name__ - )) + raise TypeError( + "{} must have one parameter declared with type 'cmd2.plugin.PostparsingData'".format(func.__name__) + ) if signature.return_annotation != plugin.PostparsingData: - raise TypeError("{} must declare return a return type of 'cmd2.plugin.PostparsingData'".format( - func.__name__ - )) + raise TypeError("{} must declare return a return type of 'cmd2.plugin.PostparsingData'".format(func.__name__)) def register_postparsing_hook(self, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None: """Register a function to be called after parsing user input but before running the command""" @@ -4726,23 +4901,18 @@ class Cmd(cmd.Cmd): paramname = list(signature.parameters.keys())[0] param = signature.parameters[paramname] if param.annotation != data_type: - raise TypeError('argument 1 of {} has incompatible type {}, expected {}'.format( - func.__name__, - param.annotation, - data_type, - )) + raise TypeError( + 'argument 1 of {} has incompatible type {}, expected {}'.format(func.__name__, param.annotation, data_type,) + ) # validate the return value has the right annotation if signature.return_annotation == signature.empty: - raise TypeError('{} does not have a declared return type, expected {}'.format( - func.__name__, - data_type, - )) + raise TypeError('{} does not have a declared return type, expected {}'.format(func.__name__, data_type,)) if signature.return_annotation != data_type: - raise TypeError('{} has incompatible return type {}, expected {}'.format( - func.__name__, - signature.return_annotation, - data_type, - )) + raise TypeError( + '{} has incompatible return type {}, expected {}'.format( + func.__name__, signature.return_annotation, data_type, + ) + ) def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None: """Register a hook to be called before the command function.""" @@ -4755,28 +4925,28 @@ class Cmd(cmd.Cmd): self._postcmd_hooks.append(func) @classmethod - def _validate_cmdfinalization_callable(cls, func: Callable[[plugin.CommandFinalizationData], - plugin.CommandFinalizationData]) -> None: + def _validate_cmdfinalization_callable( + cls, func: Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData] + ) -> None: """Check parameter and return types for command finalization hooks.""" cls._validate_callable_param_count(func, 1) signature = inspect.signature(func) _, param = list(signature.parameters.items())[0] if param.annotation != plugin.CommandFinalizationData: - raise TypeError("{} must have one parameter declared with type {}".format(func.__name__, - plugin.CommandFinalizationData)) + raise TypeError( + "{} must have one parameter declared with type {}".format(func.__name__, plugin.CommandFinalizationData) + ) if signature.return_annotation != plugin.CommandFinalizationData: - raise TypeError("{} must declare return a return type of {}".format(func.__name__, - plugin.CommandFinalizationData)) + raise TypeError("{} must declare return a return type of {}".format(func.__name__, plugin.CommandFinalizationData)) - def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizationData], - plugin.CommandFinalizationData]) -> None: + def register_cmdfinalization_hook( + self, func: Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData] + ) -> None: """Register a hook to be called after a command is completed, whether it completes successfully or not.""" self._validate_cmdfinalization_callable(func) self._cmdfinalization_hooks.append(func) - def _resolve_func_self(self, - cmd_support_func: Callable, - cmd_self: Union[CommandSet, 'Cmd']) -> object: + def _resolve_func_self(self, cmd_support_func: Callable, cmd_self: Union[CommandSet, 'Cmd']) -> 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/command_definition.py b/cmd2/command_definition.py index 3f05792c..0d6f7045 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -10,10 +10,11 @@ from .exceptions import CommandSetRegistrationError # Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues try: # pragma: no cover from typing import TYPE_CHECKING + if TYPE_CHECKING: import cmd2 -except ImportError: # pragma: no cover +except ImportError: # pragma: no cover pass @@ -40,22 +41,27 @@ def with_default_category(category: str, *, heritable: bool = True): if heritable: setattr(cls, CLASS_ATTR_DEFAULT_HELP_CATEGORY, category) - from .constants import CMD_ATTR_HELP_CATEGORY import inspect + + from .constants import CMD_ATTR_HELP_CATEGORY from .decorators import with_category + # get members of the class that meet the following criteria: # 1. Must be a function # 2. Must start with COMMAND_FUNC_PREFIX (do_) # 3. Must be a member of the class being decorated and not one inherited from a parent declaration methods = inspect.getmembers( cls, - predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX) - and meth in inspect.getmro(cls)[0].__dict__.values()) + predicate=lambda meth: inspect.isfunction(meth) + and meth.__name__.startswith(COMMAND_FUNC_PREFIX) + and meth in inspect.getmro(cls)[0].__dict__.values(), + ) category_decorator = with_category(category) for method in methods: if not hasattr(method[1], CMD_ATTR_HELP_CATEGORY): setattr(cls, method[0], category_decorator(method[1])) return cls + return decorate_class diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 4ee61754..208b8e64 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -28,12 +28,16 @@ def with_category(category: str) -> Callable: For an alternative approach to categorizing commands using a function, see :func:`~cmd2.utils.categorize` """ + def cat_decorator(func): from .utils import categorize + categorize(func, category) return func + return cat_decorator + ########################## # The _parse_positionals and _arg_swap functions allow for additional positional args to be preserved # in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be @@ -50,6 +54,7 @@ def _parse_positionals(args: Tuple) -> Tuple[Union['cmd2.Cmd', 'cmd2.CommandSet' """ for pos, arg in enumerate(args): from cmd2 import Cmd, CommandSet + if (isinstance(arg, Cmd) or isinstance(arg, CommandSet)) and len(args) > pos: if isinstance(arg, CommandSet): arg = arg._cmd @@ -72,7 +77,7 @@ def _arg_swap(args: Union[Tuple[Any], List[Any]], search_arg: Any, *replace_arg: """ 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 @@ -109,13 +114,11 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> :return: return value of command function """ cmd2_app, statement = _parse_positionals(args) - _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement, - preserve_quotes) + _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, statement, preserve_quotes) args_list = _arg_swap(args, statement, parsed_arglist) return func(*args_list, **kwargs) - 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 @@ -170,10 +173,12 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str): break -def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False) -> \ - Callable[[argparse.Namespace, List], Optional[bool]]: +def with_argparser_and_unknown_args( + parser: argparse.ArgumentParser, + *, + ns_provider: Optional[Callable[..., argparse.Namespace]] = None, + preserve_quotes: bool = False +) -> Callable[[argparse.Namespace, List], Optional[bool]]: """ Deprecated decorator. Use `with_argparser(parser, with_unknown_args=True)` instead. @@ -207,16 +212,23 @@ def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *, >>> self.poutput('unknowns: {}'.format(unknown)) """ import warnings - warnings.warn('This decorator will be deprecated. Use `with_argparser(parser, with_unknown_args=True)`.', - PendingDeprecationWarning, stacklevel=2) + + warnings.warn( + 'This decorator will be deprecated. Use `with_argparser(parser, with_unknown_args=True)`.', + PendingDeprecationWarning, + stacklevel=2, + ) return with_argparser(parser, ns_provider=ns_provider, preserve_quotes=preserve_quotes, with_unknown_args=True) -def with_argparser(parser: argparse.ArgumentParser, *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, - preserve_quotes: bool = False, - with_unknown_args: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]: +def with_argparser( + parser: argparse.ArgumentParser, + *, + ns_provider: Optional[Callable[..., argparse.Namespace]] = None, + preserve_quotes: bool = False, + with_unknown_args: bool = False +) -> Callable[[argparse.Namespace], Optional[bool]]: """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given instance of argparse.ArgumentParser. @@ -277,9 +289,9 @@ def with_argparser(parser: argparse.ArgumentParser, *, :raises: Cmd2ArgparseError if argparse has error parsing command line """ cmd2_app, statement_arg = _parse_positionals(args) - statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, - statement_arg, - preserve_quotes) + statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list( + command_name, statement_arg, preserve_quotes + ) if ns_provider is None: namespace = None @@ -294,7 +306,7 @@ def with_argparser(parser: argparse.ArgumentParser, *, if with_unknown_args: new_args = parser.parse_known_args(parsed_arglist, namespace) else: - new_args = (parser.parse_args(parsed_arglist, namespace), ) + new_args = (parser.parse_args(parsed_arglist, namespace),) ns = new_args[0] except SystemExit: raise Cmd2ArgparseError @@ -318,7 +330,7 @@ def with_argparser(parser: argparse.ArgumentParser, *, return func(*args_list, **kwargs) # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command - command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):] + command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :] _set_parser_prog(parser, command_name) # If the description has not been set, then use the method docstring if one exists @@ -338,12 +350,14 @@ def with_argparser(parser: argparse.ArgumentParser, *, return arg_decorator -def as_subcommand_to(command: str, - subcommand: str, - parser: argparse.ArgumentParser, - *, - help: Optional[str] = None, - aliases: Iterable[str] = None) -> Callable[[argparse.Namespace], Optional[bool]]: +def as_subcommand_to( + command: str, + subcommand: str, + parser: argparse.ArgumentParser, + *, + help: Optional[str] = None, + aliases: Iterable[str] = None +) -> Callable[[argparse.Namespace], Optional[bool]]: """ Tag this method as a subcommand to an existing argparse decorated command. @@ -356,6 +370,7 @@ def as_subcommand_to(command: str, ArgumentParser.add_subparser(). :return: Wrapper function that can receive an argparse.Namespace """ + def arg_decorator(func: Callable): _set_parser_prog(parser, command + ' ' + subcommand) diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index d253985a..c5a08202 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -6,11 +6,13 @@ # The following exceptions are part of the public API ############################################################################################################ + class SkipPostcommandHooks(Exception): """ Custom exception class for when a command has a failure bad enough to skip post command hooks, but not bad enough to print the exception to the user. """ + pass @@ -21,6 +23,7 @@ class Cmd2ArgparseError(SkipPostcommandHooks): loop, catch the SystemExit and raise this instead. If you still need to run post command hooks after parsing fails, just return instead of raising an exception. """ + pass @@ -29,8 +32,10 @@ class CommandSetRegistrationError(Exception): Exception that can be thrown when an error occurs while a CommandSet is being added or removed from a cmd2 application. """ + pass + ############################################################################################################ # The following exceptions are NOT part of the public API and are intended for internal use only. ############################################################################################################ @@ -38,19 +43,23 @@ class CommandSetRegistrationError(Exception): class Cmd2ShlexError(Exception): """Raised when shlex fails to parse a command line string in StatementParser""" + pass class EmbeddedConsoleExit(SystemExit): """Custom exception class for use with the py command.""" + pass class EmptyStatement(Exception): """Custom exception class for handling behavior when the user just presses <Enter>.""" + pass class RedirectionError(Exception): """Custom exception class for when redirecting or piping output fails""" + pass diff --git a/cmd2/history.py b/cmd2/history.py index 60a071fb..6c75434b 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -13,8 +13,9 @@ from .parsing import Statement @attr.s(frozen=True) -class HistoryItem(): +class HistoryItem: """Class used to represent one command in the history list""" + _listformat = ' {:>4} {}' _ex_listformat = ' {:>4}x {}' @@ -90,6 +91,7 @@ class History(list): Developers interested in accessing previously entered commands can use this class to gain access to the historical record. """ + def __init__(self, seq=()) -> None: super().__init__(seq) self.session_start_index = 0 @@ -223,7 +225,7 @@ class History(list): if include_persisted: result = self[:end] else: - result = self[self.session_start_index:end] + result = self[self.session_start_index : end] elif start is not None: # there was no separator so it's either a positive or negative integer result = [self[start]] @@ -232,7 +234,7 @@ class History(list): if include_persisted: result = self[:] else: - result = self[self.session_start_index:] + result = self[self.session_start_index :] return result def str_search(self, search: str, include_persisted: bool = False) -> List[HistoryItem]: @@ -242,6 +244,7 @@ class History(list): :param include_persisted: if True, then search full history including persisted history :return: a list of history items, or an empty list if the string was not found """ + def isin(history_item): """filter function for string search of history""" sloppy = utils.norm_fold(search) @@ -249,7 +252,7 @@ class History(list): inexpanded = sloppy in utils.norm_fold(history_item.expanded) return inraw or inexpanded - search_list = self if include_persisted else self[self.session_start_index:] + search_list = self if include_persisted else self[self.session_start_index :] return [item for item in search_list if isin(item)] def regex_search(self, regex: str, include_persisted: bool = False) -> List[HistoryItem]: @@ -268,7 +271,7 @@ class History(list): """filter function for doing a regular expression search of history""" return finder.search(hi.raw) or finder.search(hi.expanded) - search_list = self if include_persisted else self[self.session_start_index:] + search_list = self if include_persisted else self[self.session_start_index :] return [itm for itm in search_list if isin(itm)] def truncate(self, max_length: int) -> None: diff --git a/cmd2/parsing.py b/cmd2/parsing.py index c420e9aa..486cd7ed 100755 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -30,6 +30,7 @@ class MacroArg: Normal argument syntax: {5} Escaped argument syntax: {{5}} """ + # The starting index of this argument in the macro value start_index = attr.ib(validator=attr.validators.instance_of(int)) @@ -102,6 +103,7 @@ class Statement(str): 3. If you don't want to have to worry about quoted arguments, see :attr:`argv` for a trick which strips quotes off for you. """ + # the arguments, but not the command, nor the output redirection clauses. args = attr.ib(default='', validator=attr.validators.instance_of(str)) @@ -209,11 +211,14 @@ class Statement(str): class StatementParser: """Parse user input as a string into discrete command components.""" - def __init__(self, - terminators: Optional[Iterable[str]] = None, - multiline_commands: Optional[Iterable[str]] = None, - aliases: Optional[Dict[str, str]] = None, - shortcuts: Optional[Dict[str, str]] = None) -> None: + + def __init__( + self, + terminators: Optional[Iterable[str]] = None, + multiline_commands: Optional[Iterable[str]] = None, + aliases: Optional[Dict[str, str]] = None, + shortcuts: Optional[Dict[str, str]] = None, + ) -> None: """Initialize an instance of StatementParser. The following will get converted to an immutable tuple before storing internally: @@ -406,7 +411,7 @@ class StatementParser: arg_list = tokens[1:terminator_pos] # we will set the suffix later # remove all the tokens before and including the terminator - tokens = tokens[terminator_pos + 1:] + tokens = tokens[terminator_pos + 1 :] else: (testcommand, testargs) = self._command_and_args(tokens) if testcommand in self.multiline_commands: @@ -442,7 +447,7 @@ class StatementParser: if pipe_index < redir_index and pipe_index < append_index: # Get the tokens for the pipe command and expand ~ where needed - pipe_to_tokens = tokens[pipe_index + 1:] + pipe_to_tokens = tokens[pipe_index + 1 :] utils.expand_user_in_tokens(pipe_to_tokens) # Build the pipe command line string @@ -487,16 +492,18 @@ class StatementParser: multiline_command = '' # build the statement - statement = Statement(args, - raw=line, - command=command, - arg_list=arg_list, - multiline_command=multiline_command, - terminator=terminator, - suffix=suffix, - pipe_to=pipe_to, - output=output, - output_to=output_to) + statement = Statement( + args, + raw=line, + command=command, + arg_list=arg_list, + multiline_command=multiline_command, + terminator=terminator, + suffix=suffix, + pipe_to=pipe_to, + output=output, + output_to=output_to, + ) return statement def parse_command_only(self, rawinput: str) -> Statement: @@ -538,7 +545,7 @@ class StatementParser: # take everything from the end of the first match group to # the end of the line as the arguments (stripping leading # and trailing spaces) - args = line[match.end(1):].strip() + args = line[match.end(1) :].strip() # if the command is empty that means the input was either empty # or something weird like '>'. args should be empty if we couldn't # parse a command @@ -552,14 +559,12 @@ class StatementParser: multiline_command = '' # build the statement - statement = Statement(args, - raw=rawinput, - command=command, - multiline_command=multiline_command) + statement = Statement(args, raw=rawinput, command=command, multiline_command=multiline_command) return statement - def get_command_arg_list(self, command_name: str, to_parse: Union[Statement, str], - preserve_quotes: bool) -> Tuple[Statement, List[str]]: + def get_command_arg_list( + self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool + ) -> Tuple[Statement, List[str]]: """ Convenience method used by the argument parsing decorators. @@ -610,7 +615,7 @@ class StatementParser: # Check if this command matches an alias that wasn't already processed if command in remaining_aliases: # rebuild line with the expanded alias - line = self.aliases[command] + match.group(2) + line[match.end(2):] + line = self.aliases[command] + match.group(2) + line[match.end(2) :] remaining_aliases.remove(command) keep_expanding = bool(remaining_aliases) diff --git a/cmd2/plugin.py b/cmd2/plugin.py index 83093ee1..e836b9d1 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -7,6 +7,7 @@ import attr @attr.s class PostparsingData: """Data class containing information passed to postparsing hook methods""" + stop = attr.ib() statement = attr.ib() @@ -14,12 +15,14 @@ class PostparsingData: @attr.s class PrecommandData: """Data class containing information passed to precommand hook methods""" + statement = attr.ib() @attr.s class PostcommandData: """Data class containing information passed to postcommand hook methods""" + stop = attr.ib() statement = attr.ib() @@ -27,5 +30,6 @@ class PostcommandData: @attr.s class CommandFinalizationData: """Data class containing information passed to command finalization hook methods""" + stop = attr.ib() statement = attr.ib() diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 38fef142..a1dfafcb 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -47,6 +47,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr Named tuples are immutable. The contents are there for access, not for modification. """ + def __bool__(self) -> bool: """Returns True if the command succeeded, otherwise False""" @@ -61,6 +62,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr class PyBridge: """Provides a Python API wrapper for application commands.""" + def __init__(self, cmd2_app): self._cmd2_app = cmd2_app self.cmd_echo = False @@ -109,8 +111,10 @@ class PyBridge: self.stop = stop or self.stop # Save the output. If stderr is empty, set it to None. - result = CommandResult(stdout=copy_cmd_stdout.getvalue(), - stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None, - stop=stop, - data=self._cmd2_app.last_result) + result = CommandResult( + stdout=copy_cmd_stdout.getvalue(), + stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None, + stop=stop, + data=self._cmd2_app.last_result, + ) return result diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 099d76b7..aacd93cb 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -21,6 +21,7 @@ except ImportError: class RlType(Enum): """Readline library types we recognize""" + GNU = 1 PYREADLINE = 2 NONE = 3 @@ -39,9 +40,9 @@ _rl_warn_reason = '' if 'pyreadline' in sys.modules: rl_type = RlType.PYREADLINE + import atexit from ctypes import byref from ctypes.wintypes import DWORD, HANDLE - import atexit # Check if we are running in a terminal if sys.stdout.isatty(): # pragma: no cover @@ -105,7 +106,7 @@ if 'pyreadline' in sys.modules: saved_cursor = readline.rl.mode._history.history_cursor # Delete the history item - del(readline.rl.mode._history.history[pos]) + del readline.rl.mode._history.history[pos] # Update the cursor if needed if saved_cursor > pos: @@ -119,10 +120,13 @@ elif 'gnureadline' in sys.modules or 'readline' in sys.modules: try: # Load the readline lib so we can access members of it import ctypes + readline_lib = ctypes.CDLL(readline.__file__) except AttributeError: # pragma: no cover - _rl_warn_reason = ("this application is running in a non-standard Python environment in\n" - "which readline is not loaded dynamically from a shared library file.") + _rl_warn_reason = ( + "this application is running in a non-standard Python environment in\n" + "which readline is not loaded dynamically from a shared library file." + ) else: rl_type = RlType.GNU vt100_support = sys.stdout.isatty() @@ -130,10 +134,11 @@ elif 'gnureadline' in sys.modules or 'readline' in sys.modules: # Check if readline was loaded if rl_type == RlType.NONE: # pragma: no cover if not _rl_warn_reason: - _rl_warn_reason = ("no supported version of readline was found. To resolve this, install\n" - "pyreadline on Windows or gnureadline on Mac.") - rl_warning = ("Readline features including tab completion have been disabled because\n" - + _rl_warn_reason + '\n\n') + _rl_warn_reason = ( + "no supported version of readline was found. To resolve this, install\n" + "pyreadline on Windows or gnureadline on Mac." + ) + rl_warning = "Readline features including tab completion have been disabled because\n" + _rl_warn_reason + '\n\n' else: rl_warning = '' diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 7a5c826c..fba7f864 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -41,6 +41,7 @@ SPACE = ' ' class HorizontalAlignment(Enum): """Horizontal alignment of text in a cell""" + LEFT = 1 CENTER = 2 RIGHT = 3 @@ -48,6 +49,7 @@ class HorizontalAlignment(Enum): class VerticalAlignment(Enum): """Vertical alignment of text in a cell""" + TOP = 1 MIDDLE = 2 BOTTOM = 3 @@ -55,12 +57,18 @@ class VerticalAlignment(Enum): class Column: """Table column configuration""" - def __init__(self, header: str, *, width: Optional[int] = None, - header_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, - header_vert_align: VerticalAlignment = VerticalAlignment.BOTTOM, - data_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, - data_vert_align: VerticalAlignment = VerticalAlignment.TOP, - max_data_lines: Union[int, float] = constants.INFINITY) -> None: + + def __init__( + self, + header: str, + *, + width: Optional[int] = None, + header_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, + header_vert_align: VerticalAlignment = VerticalAlignment.BOTTOM, + data_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, + data_vert_align: VerticalAlignment = VerticalAlignment.TOP, + max_data_lines: Union[int, float] = constants.INFINITY + ) -> None: """ Column initializer @@ -114,6 +122,7 @@ class TableCreator: implement a more granular API rather than use TableCreator directly. There are ready-to-use examples of this defined after this class. """ + def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None: """ TableCreator initializer @@ -201,6 +210,7 @@ class TableCreator: :param max_lines: maximum lines to wrap before ending the last line displayed with an ellipsis :return: wrapped text """ + def add_word(word_to_add: str, is_last_word: bool): """ Called from loop to add a word to the wrapped text @@ -231,10 +241,9 @@ class TableCreator: room_to_add = False if room_to_add: - wrapped_word, lines_used, cur_line_width = TableCreator._wrap_long_word(word_to_add, - max_width, - max_lines - total_lines + 1, - is_last_word) + wrapped_word, lines_used, cur_line_width = TableCreator._wrap_long_word( + word_to_add, max_width, max_lines - total_lines + 1, is_last_word + ) # Write the word to the buffer wrapped_buf.write(wrapped_word) total_lines += lines_used - 1 @@ -375,8 +384,15 @@ class TableCreator: cell_width = max([ansi.style_aware_wcswidth(line) for line in lines]) return lines, cell_width - def generate_row(self, *, row_data: Optional[Sequence[Any]] = None, fill_char: str = SPACE, - pre_line: str = EMPTY, inter_cell: str = (2 * SPACE), post_line: str = EMPTY) -> str: + def generate_row( + self, + *, + row_data: Optional[Sequence[Any]] = None, + fill_char: str = SPACE, + pre_line: str = EMPTY, + inter_cell: str = (2 * SPACE), + post_line: str = EMPTY + ) -> str: """ Generate a header or data table row @@ -396,8 +412,10 @@ class TableCreator: :raises: ValueError if fill_char, pre_line, inter_cell, or post_line contains an unprintable character like a newline """ + class Cell: """Inner class which represents a table cell""" + def __init__(self) -> None: # Data in this cell split into individual lines self.lines = [] @@ -424,8 +442,7 @@ class TableCreator: raise TypeError("Fill character must be exactly one character long") # Look for unprintable characters - validation_dict = {'fill_char': fill_char, 'pre_line': pre_line, - 'inter_cell': inter_cell, 'post_line': post_line} + validation_dict = {'fill_char': fill_char, 'pre_line': pre_line, 'inter_cell': inter_cell, 'post_line': post_line} for key, val in validation_dict.items(): if ansi.style_aware_wcswidth(val) == -1: raise (ValueError("{} contains an unprintable character".format(key))) @@ -500,6 +517,7 @@ class SimpleTable(TableCreator): Implementation of TableCreator which generates a borderless table with an optional divider row after the header. This class can be used to create the whole table at once or one row at a time. """ + # Spaces between cells INTER_CELL = 2 * SPACE @@ -584,8 +602,7 @@ class SimpleTable(TableCreator): """ return self.generate_row(row_data=row_data, inter_cell=self.INTER_CELL) - def generate_table(self, table_data: Sequence[Sequence[Any]], *, - include_header: bool = True, row_spacing: int = 1) -> str: + def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True, row_spacing: int = 1) -> str: """ Generate a table from a data set @@ -623,8 +640,8 @@ class BorderedTable(TableCreator): Implementation of TableCreator which generates a table with borders around the table and between rows. Borders between columns can also be toggled. This class can be used to create the whole table at once or one row at a time. """ - def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, - column_borders: bool = True, padding: int = 1) -> None: + + def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, column_borders: bool = True, padding: int = 1) -> None: """ BorderedTable initializer @@ -685,8 +702,9 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╗' - return self.generate_row(row_data=self.empty_data, fill_char='═', pre_line=pre_line, - inter_cell=inter_cell, post_line=post_line) + return self.generate_row( + 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): """Generate a border which appears at the bottom of the header""" @@ -699,8 +717,9 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╣' - return self.generate_row(row_data=self.empty_data, fill_char='═', pre_line=pre_line, - inter_cell=inter_cell, post_line=post_line) + return self.generate_row( + 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): """Generate a border which appears at the bottom of rows""" @@ -713,8 +732,9 @@ class BorderedTable(TableCreator): post_line = self.padding * '─' + '╢' - return self.generate_row(row_data=self.empty_data, fill_char='─', pre_line=pre_line, - inter_cell=inter_cell, post_line=post_line) + return self.generate_row( + 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): """Generate a border which appears at the bottom of the table""" @@ -727,8 +747,9 @@ class BorderedTable(TableCreator): post_line = self.padding * '═' + '╝' - return self.generate_row(row_data=self.empty_data, fill_char='═', pre_line=pre_line, - inter_cell=inter_cell, post_line=post_line) + return self.generate_row( + row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + ) def generate_header(self) -> str: """Generate table header""" @@ -807,8 +828,17 @@ class AlternatingTable(BorderedTable): Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines. This class can be used to create the whole table at once or one row at a time. """ - def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, column_borders: bool = True, padding: int = 1, - bg_odd: Optional[ansi.bg] = None, bg_even: Optional[ansi.bg] = ansi.bg.bright_black) -> None: + + def __init__( + self, + cols: Sequence[Column], + *, + tab_width: int = 4, + column_borders: bool = True, + padding: int = 1, + bg_odd: Optional[ansi.bg] = None, + bg_even: Optional[ansi.bg] = ansi.bg.bright_black + ) -> None: """ AlternatingTable initializer @@ -866,8 +896,9 @@ class AlternatingTable(BorderedTable): # Apply appropriate background color to data, but don't change the original to_display = [self._apply_bg_color(col) for col in row_data] - row = self.generate_row(row_data=to_display, fill_char=fill_char, pre_line=pre_line, - inter_cell=inter_cell, post_line=post_line) + row = self.generate_row( + row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line + ) self.row_num += 1 return row diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 940c97db..563756ed 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -25,6 +25,7 @@ class Cmd2TestCase(unittest.TestCase): See example.py """ + cmdapp = None def setUp(self): @@ -67,7 +68,7 @@ class Cmd2TestCase(unittest.TestCase): finished = True break line_num += 1 - command = [line[len(self.cmdapp.visible_prompt):]] + command = [line[len(self.cmdapp.visible_prompt) :]] try: line = next(transcript) except StopIteration: @@ -75,12 +76,13 @@ class Cmd2TestCase(unittest.TestCase): line_num += 1 # Read the entirety of a multi-line command while line.startswith(self.cmdapp.continuation_prompt): - command.append(line[len(self.cmdapp.continuation_prompt):]) + command.append(line[len(self.cmdapp.continuation_prompt) :]) try: line = next(transcript) except StopIteration as exc: - msg = 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, - command[0]) + msg = 'Transcript broke off while reading command beginning at line {} with\n{}'.format( + line_num, command[0] + ) raise StopIteration(msg) from exc line_num += 1 command = ''.join(command) @@ -91,7 +93,8 @@ class Cmd2TestCase(unittest.TestCase): # Read the expected result from transcript if ansi.strip_style(line).startswith(self.cmdapp.visible_prompt): message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format( - fname, line_num, command, result) + fname, line_num, command, result + ) self.assertTrue(not (result.strip()), message) # If the command signaled the application to quit there should be no more commands self.assertFalse(stop, stop_msg) @@ -114,7 +117,8 @@ class Cmd2TestCase(unittest.TestCase): expected = ''.join(expected) expected = self._transform_transcript_expected(expected) message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format( - fname, line_num, command, expected, result) + fname, line_num, command, expected, result + ) self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message) def _transform_transcript_expected(self, s: str) -> str: @@ -160,7 +164,7 @@ class Cmd2TestCase(unittest.TestCase): else: # No closing slash, we have to add the first slash, # and the rest of the text - regex += re.escape(s[start - 1:]) + regex += re.escape(s[start - 1 :]) break return regex @@ -187,18 +191,18 @@ class Cmd2TestCase(unittest.TestCase): break else: # check if the slash is preceeded by a backslash - if s[pos - 1:pos] == '\\': + if s[pos - 1 : pos] == '\\': # it is. if in_regex: # add everything up to the backslash as a # regular expression - regex += s[start:pos - 1] + regex += s[start : pos - 1] # skip the backslash, and add the slash regex += s[pos] else: # add everything up to the backslash as escaped # plain text - regex += re.escape(s[start:pos - 1]) + regex += re.escape(s[start : pos - 1]) # and then add the slash as escaped # plain text regex += re.escape(s[pos]) diff --git a/cmd2/utils.py b/cmd2/utils.py index b58cdb96..d2dd5b18 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -12,10 +12,9 @@ import re import subprocess import sys import threading - import unicodedata from enum import Enum -from typing import Any, Callable, Dict, IO, Iterable, List, Optional, TextIO, Type, Union +from typing import IO, Any, Callable, Dict, Iterable, List, Optional, TextIO, Type, Union from . import constants @@ -88,6 +87,7 @@ class CompletionError(Exception): - A previous command line argument that determines the data set being completed is invalid - Tab completion hints """ + def __init__(self, *args, apply_style: bool = True, **kwargs): """ Initializer for CompletionError @@ -103,13 +103,20 @@ class CompletionError(Exception): class Settable: """Used to configure a cmd2 instance member to be settable via the set command in the CLI""" - def __init__(self, name: str, val_type: Callable, description: str, *, - onchange_cb: Callable[[str, Any, Any], Any] = None, - choices: Iterable = None, - choices_function: Optional[Callable] = None, - choices_method: Optional[Callable] = None, - completer_function: Optional[Callable] = None, - completer_method: Optional[Callable] = None): + + def __init__( + self, + name: str, + val_type: Callable, + description: str, + *, + onchange_cb: Callable[[str, Any, Any], Any] = None, + choices: Iterable = None, + choices_function: Optional[Callable] = None, + choices_method: Optional[Callable] = None, + completer_function: Optional[Callable] = None, + completer_method: Optional[Callable] = None + ): """ Settable Initializer @@ -158,8 +165,7 @@ class Settable: self.completer_method = completer_method -def namedtuple_with_defaults(typename: str, field_names: Union[str, List[str]], - default_values: collections_abc.Iterable = ()): +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 @@ -446,8 +452,8 @@ class StdSim: Class to simulate behavior of sys.stdout or sys.stderr. 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, *, 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. @@ -530,6 +536,7 @@ class ByteBuf: """ Used by StdSim to write binary data and stores the actual bytes written """ + # Used to know when to flush the StdSim NEWLINES = [b'\n', b'\r'] @@ -560,8 +567,8 @@ class ProcReader: Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE. If neither are pipes, then the process will run normally and no output will be captured. """ - def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, TextIO], - stderr: Union[StdSim, TextIO]) -> None: + + def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, TextIO], stderr: Union[StdSim, TextIO]) -> None: """ ProcReader initializer :param proc: the Popen process being read from @@ -572,11 +579,9 @@ class ProcReader: self._stdout = stdout self._stderr = stderr - self._out_thread = threading.Thread(name='out_thread', target=self._reader_thread_func, - kwargs={'read_stdout': True}) + self._out_thread = threading.Thread(name='out_thread', target=self._reader_thread_func, kwargs={'read_stdout': True}) - self._err_thread = threading.Thread(name='out_thread', target=self._reader_thread_func, - kwargs={'read_stdout': False}) + self._err_thread = threading.Thread(name='out_thread', target=self._reader_thread_func, kwargs={'read_stdout': False}) # Start the reader threads for pipes only if self._proc.stdout is not None: @@ -587,6 +592,7 @@ class ProcReader: def send_sigint(self) -> None: """Send a SIGINT to the process similar to if <Ctrl>+C were pressed""" import signal + if sys.platform.startswith('win'): # cmd2 started the Windows process in a new process group. Therefore # a CTRL_C_EVENT can't be sent to it. Send a CTRL_BREAK_EVENT instead. @@ -664,6 +670,7 @@ class ContextFlag: while a critical code section has set the flag to True. Because signal handling is always done on the main thread, this class is not thread-safe since there is no need. """ + def __init__(self) -> None: # When this flag has a positive value, it is considered set. # When it is 0, it is not set. It should never go below 0. @@ -683,8 +690,14 @@ class ContextFlag: class RedirectionSavedState: """Created by each command to store information required to restore state after redirection""" - def __init__(self, self_stdout: Union[StdSim, IO[str]], sys_stdout: Union[StdSim, IO[str]], - pipe_proc_reader: Optional[ProcReader], saved_redirecting: bool) -> None: + + def __init__( + self, + self_stdout: Union[StdSim, IO[str]], + sys_stdout: Union[StdSim, IO[str]], + pipe_proc_reader: Optional[ProcReader], + saved_redirecting: bool, + ) -> None: """ RedirectionSavedState initializer :param self_stdout: saved value of Cmd.stdout @@ -722,13 +735,21 @@ def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against class TextAlignment(Enum): """Horizontal text alignment""" + LEFT = 1 CENTER = 2 RIGHT = 3 -def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', - width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: +def align_text( + text: str, + alignment: TextAlignment, + *, + fill_char: str = ' ', + width: Optional[int] = None, + tab_width: int = 4, + truncate: bool = False +) -> str: """ Align text for display within a given width. Supports characters with display widths greater than 1. ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned @@ -809,7 +830,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', line_width = ansi.style_aware_wcswidth(line) if line_width == -1: - raise(ValueError("Text to align contains an unprintable character")) + raise (ValueError("Text to align contains an unprintable character")) # Get the styles in this line line_styles = get_styles_in_text(line) @@ -860,8 +881,9 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', return text_buf.getvalue() -def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, - tab_width: int = 4, truncate: bool = False) -> str: +def align_left( + text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False +) -> str: """ Left align text for display within a given width. Supports characters with display widths greater than 1. ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned @@ -879,12 +901,12 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :raises: ValueError if text or fill_char contains an unprintable character :raises: ValueError if width is less than 1 """ - return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, - tab_width=tab_width, truncate=truncate) + return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width, truncate=truncate) -def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, - tab_width: int = 4, truncate: bool = False) -> str: +def align_center( + text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False +) -> str: """ Center text for display within a given width. Supports characters with display widths greater than 1. ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned @@ -902,12 +924,12 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None :raises: ValueError if text or fill_char contains an unprintable character :raises: ValueError if width is less than 1 """ - return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, - tab_width=tab_width, truncate=truncate) + return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width, truncate=truncate) -def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, - tab_width: int = 4, truncate: bool = False) -> str: +def align_right( + text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False +) -> str: """ Right align text for display within a given width. Supports characters with display widths greater than 1. ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned @@ -925,8 +947,7 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :raises: ValueError if text or fill_char contains an unprintable character :raises: ValueError if width is less than 1 """ - return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, - tab_width=tab_width, truncate=truncate) + return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width, truncate=truncate) def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: @@ -951,6 +972,7 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: :raises: ValueError if max_width is less than 1 """ import io + from . import ansi # Handle tabs @@ -1077,16 +1099,15 @@ def get_defining_class(meth) -> 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__')): + if inspect.ismethod(meth) or ( + inspect.isbuiltin(meth) and getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__') + ): for cls in inspect.getmro(meth.__self__.__class__): if meth.__name__ in cls.__dict__: return cls meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing if inspect.isfunction(meth): - cls = getattr(inspect.getmodule(meth), - meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0]) + 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 |