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 | |
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.
95 files changed, 2964 insertions, 1929 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..7c75c7ef --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 + hooks: + # - id: check-json + - id: check-yaml + - repo: https://github.com/ambv/black + rev: 19.10b0 + hooks: + - id: black + args: ["-l 127", "--skip-string-normalization"] + - repo: https://github.com/pycqa/isort + rev: 5.6.4 + hooks: + - id: isort + - repo: https://github.com/asottile/pyupgrade + rev: v2.7.3 + hooks: + - id: pyupgrade + # args: ["--py36-plus"] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + # - repo: https://github.com/PyCQA/doc8.git + # rev: 0.8.1 + # hooks: + # - id: doc8 + # args: ["-q"]
\ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6a9cdac2..fcbd5030 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ matrix: # - BREW_INSTALL=python3 install: - - if [[ $TRAVIS_PYTHON_VERSION == 3.5.2 ]]; then pip install flake8 nox==2019.11.9; else pip install flake8 nox; fi + - if [[ $TRAVIS_PYTHON_VERSION == 3.5.2 ]]; then pip install nox==2019.11.9 pre-commit; else pip install nox pre-commit; fi # - | # if [[ $TRAVIS_OS_NAME == 'osx' ]]; then # if [[ -n "$BREW_INSTALL" ]]; then @@ -53,8 +53,9 @@ install: before_script: # stop the build if there are Python syntax errors or undefined names # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # Reads from setup.cfg for configuration options. if [[ $NOXSESSION == tests-3.8 ]]; then - flake8 . --count --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics ; + pre-commit run -a; fi script: @@ -22,6 +22,7 @@ ipython = "*" isort = "*" mock = {version = "*",markers = "python_version < '3.6'"} plumbum = "*" +pre-commit = "*" pyreadline = {version = "*",sys_platform = "== 'win32'"} pytest = "*" pytest-cov = "*" 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 diff --git a/docs/conf.py b/docs/conf.py index 9396f000..9ad7b0da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,11 +30,13 @@ import cmd2 # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.autosectionlabel', - 'sphinx.ext.intersphinx', - 'sphinx.ext.doctest', - 'sphinx.ext.todo'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.intersphinx', + 'sphinx.ext.doctest', + 'sphinx.ext.todo', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -116,15 +118,12 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -134,8 +133,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'cmd2.tex', 'cmd2 Documentation', - 'Catherine Devlin and Todd Leonhardt', 'manual'), + (master_doc, 'cmd2.tex', 'cmd2 Documentation', 'Catherine Devlin and Todd Leonhardt', 'manual'), ] @@ -143,10 +141,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'cmd2', 'cmd2 Documentation', - [author], 1) -] +man_pages = [(master_doc, 'cmd2', 'cmd2 Documentation', [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -155,9 +150,15 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'cmd2', 'cmd2 Documentation', - author, 'cmd2', 'A python package for building powerful command-line interpreter (CLI) programs.', - 'Miscellaneous'), + ( + master_doc, + 'cmd2', + 'cmd2 Documentation', + author, + 'cmd2', + 'A python package for building powerful command-line interpreter (CLI) programs.', + 'Miscellaneous', + ), ] @@ -166,9 +167,7 @@ texinfo_documents = [ intersphinx_mapping = {'http://docs.python.org/': None} # options for autodoc -autodoc_default_options = { - 'member-order': 'bysource' -} +autodoc_default_options = {'member-order': 'bysource'} # Ignore nitpicky warnings from autodoc which are occurring for very new versions of Sphinx and autodoc # They seem to be happening because autodoc is now trying to add hyperlinks to docs for typehint classes diff --git a/examples/alias_startup.py b/examples/alias_startup.py index 3fa9ec5a..3ddcc2fe 100755 --- a/examples/alias_startup.py +++ b/examples/alias_startup.py @@ -23,5 +23,6 @@ class AliasAndStartup(cmd2.Cmd): if __name__ == '__main__': import sys + app = AliasAndStartup() sys.exit(app.cmdloop()) diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py index a085341d..f4d92624 100755 --- a/examples/arg_decorators.py +++ b/examples/arg_decorators.py @@ -14,11 +14,9 @@ class ArgparsingApp(cmd2.Cmd): # do_fsize parser fsize_parser = cmd2.Cmd2ArgumentParser(description='Obtain the size of a file') - fsize_parser.add_argument('-c', '--comma', action='store_true', - help='add comma for thousands separator') + fsize_parser.add_argument('-c', '--comma', action='store_true', help='add comma for thousands separator') fsize_parser.add_argument('-u', '--unit', choices=['MB', 'KB'], help='unit to display size in') - fsize_parser.add_argument('file_path', help='path of file', - completer_method=cmd2.Cmd.path_complete) + fsize_parser.add_argument('file_path', help='path of file', completer_method=cmd2.Cmd.path_complete) @cmd2.with_argparser(fsize_parser) def do_fsize(self, args: argparse.Namespace) -> None: diff --git a/examples/arg_print.py b/examples/arg_print.py index dbf740ff..6736c74f 100755 --- a/examples/arg_print.py +++ b/examples/arg_print.py @@ -64,5 +64,6 @@ class ArgumentAndOptionPrinter(cmd2.Cmd): if __name__ == '__main__': import sys + app = ArgumentAndOptionPrinter() sys.exit(app.cmdloop()) diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index e44533b3..39904dec 100644 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -30,12 +30,7 @@ def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[s def choices_completion_item() -> List[CompletionItem]: """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" - items = \ - { - 1: "My item", - 2: "Another item", - 3: "Yet another item" - } + items = {1: "My item", 2: "Another item", 3: "Yet another item"} return [CompletionItem(item_id, description) for item_id, description in items.items()] @@ -77,38 +72,48 @@ class ArgparseCompletion(Cmd): raise CompletionError("debug must be true") # Parser for example command - example_parser = Cmd2ArgumentParser(description="Command demonstrating tab completion with argparse\n" - "Notice even the flags of this command tab complete") + example_parser = Cmd2ArgumentParser( + description="Command demonstrating tab completion with argparse\n" "Notice even the flags of this command tab complete" + ) # Tab complete from a list using argparse choices. Set metavar if you don't # want the entire choices list showing in the usage text for this command. - example_parser.add_argument('--choices', choices=food_item_strs, metavar="CHOICE", - help="tab complete using choices") + example_parser.add_argument('--choices', choices=food_item_strs, metavar="CHOICE", help="tab complete using choices") # Tab complete from choices provided by a choices function and choices method - example_parser.add_argument('--choices_function', choices_function=choices_function, - help="tab complete using a choices_function") - example_parser.add_argument('--choices_method', choices_method=choices_method, - help="tab complete using a choices_method") + example_parser.add_argument( + '--choices_function', choices_function=choices_function, help="tab complete using a choices_function" + ) + example_parser.add_argument('--choices_method', choices_method=choices_method, help="tab complete using a choices_method") # Tab complete using a completer function and completer method - example_parser.add_argument('--completer_function', completer_function=completer_function, - help="tab complete using a completer_function") - example_parser.add_argument('--completer_method', completer_method=Cmd.path_complete, - help="tab complete using a completer_method") + example_parser.add_argument( + '--completer_function', completer_function=completer_function, help="tab complete using a completer_function" + ) + example_parser.add_argument( + '--completer_method', completer_method=Cmd.path_complete, help="tab complete using a completer_method" + ) # Demonstrate raising a CompletionError while tab completing - example_parser.add_argument('--completion_error', choices_method=choices_completion_error, - help="raise a CompletionError while tab completing if debug is False") + example_parser.add_argument( + '--completion_error', + choices_method=choices_completion_error, + help="raise a CompletionError while tab completing if debug is False", + ) # Demonstrate returning CompletionItems instead of strings - example_parser.add_argument('--completion_item', choices_function=choices_completion_item, metavar="ITEM_ID", - descriptive_header="Description", - help="demonstrate use of CompletionItems") + example_parser.add_argument( + '--completion_item', + choices_function=choices_completion_item, + metavar="ITEM_ID", + descriptive_header="Description", + help="demonstrate use of CompletionItems", + ) # Demonstrate use of arg_tokens dictionary - example_parser.add_argument('--arg_tokens', choices_function=choices_arg_tokens, - help="demonstrate use of arg_tokens dictionary") + example_parser.add_argument( + '--arg_tokens', choices_function=choices_arg_tokens, help="demonstrate use of arg_tokens dictionary" + ) @with_argparser(example_parser) def do_example(self, _: argparse.Namespace) -> None: @@ -118,5 +123,6 @@ class ArgparseCompletion(Cmd): if __name__ == '__main__': import sys + app = ArgparseCompletion() sys.exit(app.cmdloop()) diff --git a/examples/async_printing.py b/examples/async_printing.py index a136d8e2..d7304661 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -12,17 +12,18 @@ from typing import List import cmd2 from cmd2 import fg, style -ALERTS = ["Watch as this application prints alerts and updates the prompt", - "This will only happen when the prompt is present", - "Notice how it doesn't interfere with your typing or cursor location", - "Go ahead and type some stuff and move the cursor throughout the line", - "Keep typing...", - "Move that cursor...", - "Pretty seamless, eh?", - "Feedback can also be given in the window title. Notice the alert count up there?", - "You can stop and start the alerts by typing stop_alerts and start_alerts", - "This demo will now continue to print alerts at random intervals" - ] +ALERTS = [ + "Watch as this application prints alerts and updates the prompt", + "This will only happen when the prompt is present", + "Notice how it doesn't interfere with your typing or cursor location", + "Go ahead and type some stuff and move the cursor throughout the line", + "Keep typing...", + "Move that cursor...", + "Pretty seamless, eh?", + "Feedback can also be given in the window title. Notice the alert count up there?", + "You can stop and start the alerts by typing stop_alerts and start_alerts", + "This demo will now continue to print alerts at random intervals", +] class AlerterApp(cmd2.Cmd): @@ -196,6 +197,7 @@ class AlerterApp(cmd2.Cmd): if __name__ == '__main__': import sys + app = AlerterApp() app.set_window_title("Asynchronous Printer Test") sys.exit(app.cmdloop()) diff --git a/examples/basic.py b/examples/basic.py index 2a1e9a12..3d703950 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -16,8 +16,12 @@ class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' def __init__(self): - super().__init__(multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', use_ipython=True) + super().__init__( + multiline_commands=['echo'], + persistent_history_file='cmd2_history.dat', + startup_script='scripts/startup.txt', + use_ipython=True, + ) self.intro = style('Welcome to PyOhio 2019 and cmd2!', fg=fg.red, bg=bg.white, bold=True) + ' π' diff --git a/examples/basic_completion.py b/examples/basic_completion.py index f33029c9..aab7d3d1 100755 --- a/examples/basic_completion.py +++ b/examples/basic_completion.py @@ -22,14 +22,13 @@ food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] # This data is used to demonstrate delimiter_complete -file_strs = \ - [ - '/home/user/file.db', - '/home/user/file space.db', - '/home/user/another.db', - '/home/other user/maps.db', - '/home/other user/tests.db' - ] +file_strs = [ + '/home/user/file.db', + '/home/user/file space.db', + '/home/user/another.db', + '/home/other user/maps.db', + '/home/other user/tests.db', +] class BasicCompletion(cmd2.Cmd): @@ -46,20 +45,17 @@ class BasicCompletion(cmd2.Cmd): def complete_flag_based(self, text, line, begidx, endidx) -> List[str]: """Completion function for do_flag_based""" - flag_dict = \ - { - # Tab complete food items after -f and --food flags in command line - '-f': food_item_strs, - '--food': food_item_strs, - - # Tab complete sport items after -s and --sport flags in command line - '-s': sport_item_strs, - '--sport': sport_item_strs, - - # Tab complete using path_complete function after -p and --path flags in command line - '-p': self.path_complete, - '--path': self.path_complete, - } + flag_dict = { + # Tab complete food items after -f and --food flags in command line + '-f': food_item_strs, + '--food': food_item_strs, + # Tab complete sport items after -s and --sport flags in command line + '-s': sport_item_strs, + '--sport': sport_item_strs, + # Tab complete using path_complete function after -p and --path flags in command line + '-p': self.path_complete, + '--path': self.path_complete, + } return self.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) @@ -69,12 +65,11 @@ class BasicCompletion(cmd2.Cmd): def complete_index_based(self, text, line, begidx, endidx) -> List[str]: """Completion function for do_index_based""" - index_dict = \ - { - 1: food_item_strs, # Tab complete food items at index 1 in command line - 2: sport_item_strs, # Tab complete sport items at index 2 in command line - 3: self.path_complete, # Tab complete using path_complete function at index 3 in command line - } + index_dict = { + 1: food_item_strs, # Tab complete food items at index 1 in command line + 2: sport_item_strs, # Tab complete sport items at index 2 in command line + 3: self.path_complete, # Tab complete using path_complete function at index 3 in command line + } return self.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) @@ -83,8 +78,7 @@ class BasicCompletion(cmd2.Cmd): self.poutput("Args: {}".format(statement.args)) # Use a partialmethod to set arguments to delimiter_complete - complete_delimiter_complete = functools.partialmethod(cmd2.Cmd.delimiter_complete, - match_against=file_strs, delimiter='/') + complete_delimiter_complete = functools.partialmethod(cmd2.Cmd.delimiter_complete, match_against=file_strs, delimiter='/') def do_raise_error(self, statement: cmd2.Statement): """Demonstrates effect of raising CompletionError""" @@ -103,5 +97,6 @@ class BasicCompletion(cmd2.Cmd): if __name__ == '__main__': import sys + app = BasicCompletion() sys.exit(app.cmdloop()) diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index b65bcbcb..73f0dc22 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -50,7 +50,7 @@ class CmdLineApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -72,13 +72,13 @@ class CmdLineApp(cmd2.Cmd): repetitions = args.repeat or 1 for i in range(min(repetitions, self.maxrepeats)): output = [] - if random.random() < .33: + if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) for word in args.words: - if random.random() < .40: + if random.random() < 0.40: output.append(random.choice(self.MUMBLES)) output.append(word) - if random.random() < .25: + if random.random() < 0.25: output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) @@ -86,15 +86,11 @@ class CmdLineApp(cmd2.Cmd): def main(argv=None): """Run when invoked from the operating system shell""" - parser = argparse.ArgumentParser( - description='Commands as arguments' - ) + parser = argparse.ArgumentParser(description='Commands as arguments') command_help = 'optional command to run, if no command given, enter an interactive shell' - parser.add_argument('command', nargs='?', - help=command_help) + parser.add_argument('command', nargs='?', help=command_help) arg_help = 'optional arguments for command' - parser.add_argument('command_args', nargs=argparse.REMAINDER, - help=arg_help) + parser.add_argument('command_args', nargs=argparse.REMAINDER, help=arg_help) args = parser.parse_args(argv) @@ -113,4 +109,5 @@ def main(argv=None): if __name__ == '__main__': import sys + sys.exit(main()) diff --git a/examples/colors.py b/examples/colors.py index 8f8e3c6e..9d02c166 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -34,6 +34,7 @@ from cmd2 import ansi class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" + def __init__(self): # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell super().__init__(use_ipython=True) @@ -61,7 +62,7 @@ class CmdLineApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -98,5 +99,6 @@ class CmdLineApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = CmdLineApp() sys.exit(c.cmdloop()) diff --git a/examples/custom_parser.py b/examples/custom_parser.py index 34c7bee2..fa21a0b4 100644 --- a/examples/custom_parser.py +++ b/examples/custom_parser.py @@ -10,6 +10,7 @@ from cmd2 import Cmd2ArgumentParser, ansi, set_default_argument_parser # First define the parser class CustomParser(Cmd2ArgumentParser): """Overrides error class""" + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/examples/decorator_example.py b/examples/decorator_example.py index 1b6d7570..d661eeba 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -18,12 +18,14 @@ import cmd2 class CmdLineApp(cmd2.Cmd): """ Example cmd2 application. """ + def __init__(self, ip_addr=None, port=None, transcript_files=None): shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'], - shortcuts=shortcuts) + super().__init__( + use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'], shortcuts=shortcuts + ) self.maxrepeats = 3 # Make maxrepeats settable at runtime @@ -48,7 +50,7 @@ class CmdLineApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) diff --git a/examples/default_categories.py b/examples/default_categories.py index 19699513..bee2b1ea 100644 --- a/examples/default_categories.py +++ b/examples/default_categories.py @@ -11,6 +11,7 @@ from cmd2 import CommandSet, with_default_category @with_default_category('Default Category') class MyBaseCommandSet(CommandSet): """Defines a default category for all sub-class CommandSets""" + pass @@ -18,6 +19,7 @@ class ChildInheritsParentCategories(MyBaseCommandSet): """ This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category' """ + def do_hello(self, _: cmd2.Statement): self._cmd.poutput('Hello') @@ -31,6 +33,7 @@ class ChildOverridesParentCategoriesNonHeritable(MyBaseCommandSet): This subclass overrides the 'Default Category' from the parent, but in a non-heritable fashion. Sub-classes of this CommandSet will not inherit this category and will, instead, inherit 'Default Category' """ + def do_goodbye(self, _: cmd2.Statement): self._cmd.poutput('Goodbye') @@ -40,6 +43,7 @@ class GrandchildInheritsGrandparentCategory(ChildOverridesParentCategoriesNonHer This subclass's parent class declared its default category non-heritable. Instead, it inherits the category defined by the grandparent class. """ + def do_aloha(self, _: cmd2.Statement): self._cmd.poutput('Aloha') @@ -50,6 +54,7 @@ class ChildOverridesParentCategories(MyBaseCommandSet): This subclass is decorated with a default category that is heritable. This overrides the parent class's default category declaration. """ + def do_bonjour(self, _: cmd2.Statement): self._cmd.poutput('Bonjour') @@ -59,6 +64,7 @@ class GrandchildInheritsHeritable(ChildOverridesParentCategories): This subclass's parent declares a default category that overrides its parent. As a result, commands in this CommandSet will be categorized under 'Heritable Category' """ + def do_monde(self, _: cmd2.Statement): self._cmd.poutput('Monde') diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py index 620acb7f..4fc2629f 100755 --- a/examples/dynamic_commands.py +++ b/examples/dynamic_commands.py @@ -13,6 +13,7 @@ CATEGORY = 'Dynamic Commands' class CommandsInLoop(cmd2.Cmd): """Example of dynamically adding do_* commands.""" + def __init__(self): # Add dynamic commands before calling cmd2.Cmd's init since it validates command names for command in COMMAND_LIST: diff --git a/examples/environment.py b/examples/environment.py index 670b63ac..5f5d927b 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -13,11 +13,7 @@ class EnvironmentApp(cmd2.Cmd): super().__init__() self.degrees_c = 22 self.sunny = False - self.add_settable(cmd2.Settable('degrees_c', - int, - 'Temperature in Celsius', - onchange_cb=self._onchange_degrees_c - )) + self.add_settable(cmd2.Settable('degrees_c', int, 'Temperature in Celsius', onchange_cb=self._onchange_degrees_c)) self.add_settable(cmd2.Settable('sunny', bool, 'Is it sunny outside?')) def do_sunbathe(self, arg): @@ -38,5 +34,6 @@ class EnvironmentApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = EnvironmentApp() sys.exit(c.cmdloop()) diff --git a/examples/event_loops.py b/examples/event_loops.py index 53d3ca2b..86dc01fb 100755 --- a/examples/event_loops.py +++ b/examples/event_loops.py @@ -11,6 +11,7 @@ import cmd2 class Cmd2EventBased(cmd2.Cmd): """Basic example of how to run cmd2 without it controlling the main loop.""" + def __init__(self): super().__init__() diff --git a/examples/example.py b/examples/example.py index 0272a6e5..daa07eee 100755 --- a/examples/example.py +++ b/examples/example.py @@ -46,7 +46,7 @@ class CmdLineApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -68,18 +68,19 @@ class CmdLineApp(cmd2.Cmd): repetitions = args.repeat or 1 for _ in range(min(repetitions, self.maxrepeats)): output = [] - if random.random() < .33: + if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) for word in args.words: - if random.random() < .40: + if random.random() < 0.40: output.append(random.choice(self.MUMBLES)) output.append(word) - if random.random() < .25: + if random.random() < 0.25: output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) if __name__ == '__main__': import sys + c = CmdLineApp() sys.exit(c.cmdloop()) diff --git a/examples/exit_code.py b/examples/exit_code.py index 89ed86cd..a35b4df8 100755 --- a/examples/exit_code.py +++ b/examples/exit_code.py @@ -34,6 +34,7 @@ Usage: exit [exit_code] if __name__ == '__main__': import sys + app = ReplWithExitCode() sys_exit_code = app.cmdloop() app.poutput('{!r} exiting with code: {}'.format(sys.argv[0], sys_exit_code)) diff --git a/examples/first_app.py b/examples/first_app.py index d8272e86..3330b459 100755 --- a/examples/first_app.py +++ b/examples/first_app.py @@ -41,7 +41,7 @@ class FirstApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -56,5 +56,6 @@ class FirstApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = FirstApp() sys.exit(c.cmdloop()) diff --git a/examples/hello_cmd2.py b/examples/hello_cmd2.py index 94ec334f..7bd0ef2c 100755 --- a/examples/hello_cmd2.py +++ b/examples/hello_cmd2.py @@ -7,11 +7,11 @@ from cmd2 import cmd2 if __name__ == '__main__': import sys - # If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality. + # If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality. # Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive # debugging of your application via introspection on self. app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat') - app.self_in_py = True # Enable access to "self" within the py command - app.debug = True # Show traceback if/when an exception occurs + app.self_in_py = True # Enable access to "self" within the py command + app.debug = True # Show traceback if/when an exception occurs sys.exit(app.cmdloop()) diff --git a/examples/help_categories.py b/examples/help_categories.py index 7401bafe..16860ec2 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -16,6 +16,7 @@ def my_decorator(f): def wrapper(*args, **kwds): print('Calling decorated function') return f(*args, **kwds) + return wrapper @@ -70,8 +71,9 @@ class HelpCategories(cmd2.Cmd): """Redeploy command""" self.poutput('Redeploy') - restart_parser = cmd2.DEFAULT_ARGUMENT_PARSER(description='Restart', - epilog='my_decorator does not run when argparse errors') + restart_parser = cmd2.DEFAULT_ARGUMENT_PARSER( + description='Restart', epilog='my_decorator does not run when argparse errors' + ) restart_parser.add_argument('when', choices=START_TIMES, help='Specify when to restart') @cmd2.with_argparser(restart_parser) @@ -98,15 +100,10 @@ class HelpCategories(cmd2.Cmd): self.poutput('Find Leakers') # Tag the above command functions under the category Application Management - cmd2.categorize((do_list, - do_deploy, - do_start, - do_sessions, - do_redeploy, - do_expire, - do_undeploy, - do_stop, - do_findleakers), CMD_CAT_APP_MGMT) + cmd2.categorize( + (do_list, do_deploy, do_start, do_sessions, do_redeploy, do_expire, do_undeploy, do_stop, do_findleakers), + CMD_CAT_APP_MGMT, + ) def do_resources(self, _): """Resources command""" @@ -160,8 +157,7 @@ class HelpCategories(cmd2.Cmd): @cmd2.with_category("Command Management") def do_disable_commands(self, _): """Disable the Application Management commands""" - message_to_print = "{} is not available while {} commands are disabled".format(COMMAND_NAME, - self.CMD_CAT_APP_MGMT) + message_to_print = "{} is not available while {} commands are disabled".format(COMMAND_NAME, self.CMD_CAT_APP_MGMT) self.disable_category(self.CMD_CAT_APP_MGMT, message_to_print) self.poutput("The Application Management commands have been disabled") diff --git a/examples/hooks.py b/examples/hooks.py index f8079e58..55b43e5d 100755 --- a/examples/hooks.py +++ b/examples/hooks.py @@ -60,20 +60,17 @@ class CmdLineApp(cmd2.Cmd): command_pattern = re.compile(r'^([^\s\d]+)(\d+)') match = command_pattern.search(command) if match: - data.statement = self.statement_parser.parse("{} {} {}".format( - match.group(1), - match.group(2), - '' if data.statement.args is None else data.statement.args - )) + data.statement = self.statement_parser.parse( + "{} {} {}".format(match.group(1), match.group(2), '' if data.statement.args is None else data.statement.args) + ) return data def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: """A hook to make uppercase commands lowercase.""" command = data.statement.command.lower() - data.statement = self.statement_parser.parse("{} {}".format( - command, - '' if data.statement.args is None else data.statement.args - )) + data.statement = self.statement_parser.parse( + "{} {}".format(command, '' if data.statement.args is None else data.statement.args) + ) return data def abbrev_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: @@ -106,5 +103,6 @@ class CmdLineApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = CmdLineApp() sys.exit(c.cmdloop()) diff --git a/examples/initialization.py b/examples/initialization.py index 50bb73aa..6137c6a7 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -20,8 +20,12 @@ class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' def __init__(self): - super().__init__(multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', use_ipython=True) + super().__init__( + multiline_commands=['echo'], + persistent_history_file='cmd2_history.dat', + startup_script='scripts/startup.txt', + use_ipython=True, + ) # Prints an intro banner once upon application startup self.intro = style('Welcome to cmd2!', fg=fg.red, bg=bg.white, bold=True) @@ -42,8 +46,9 @@ class BasicApp(cmd2.Cmd): self.foreground_color = 'cyan' # Make echo_fg settable at runtime - self.add_settable(cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', - choices=fg.colors())) + self.add_settable( + cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', choices=fg.colors()) + ) @cmd2.with_category(CUSTOM_CATEGORY) def do_intro(self, _): diff --git a/examples/migrating.py b/examples/migrating.py index 86a59ed6..e7c2f19e 100755 --- a/examples/migrating.py +++ b/examples/migrating.py @@ -31,18 +31,19 @@ class CmdLineApp(cmd.Cmd): """Mumbles what you tell me to.""" words = line.split(' ') output = [] - if random.random() < .33: + if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) for word in words: - if random.random() < .40: + if random.random() < 0.40: output.append(random.choice(self.MUMBLES)) output.append(word) - if random.random() < .25: + if random.random() < 0.25: output.append(random.choice(self.MUMBLE_LAST)) print(' '.join(output), file=self.stdout) if __name__ == '__main__': import sys + c = CmdLineApp() sys.exit(c.cmdloop()) diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index 2ceda439..cb1b07bc 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -15,14 +15,13 @@ class BasicCompletionCommandSet(CommandSet): sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] # This data is used to demonstrate delimiter_complete - file_strs = \ - [ - '/home/user/file.db', - '/home/user/file space.db', - '/home/user/another.db', - '/home/other user/maps.db', - '/home/other user/tests.db' - ] + file_strs = [ + '/home/user/file.db', + '/home/user/file space.db', + '/home/user/another.db', + '/home/other user/maps.db', + '/home/other user/tests.db', + ] def do_flag_based(self, cmd: Cmd, statement: Statement): """Tab completes arguments based on a preceding flag using flag_based_complete @@ -34,20 +33,17 @@ class BasicCompletionCommandSet(CommandSet): def complete_flag_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Completion function for do_flag_based""" - flag_dict = \ - { - # Tab complete food items after -f and --food flags in command line - '-f': self.food_item_strs, - '--food': self.food_item_strs, - - # Tab complete sport items after -s and --sport flags in command line - '-s': self.sport_item_strs, - '--sport': self.sport_item_strs, - - # Tab complete using path_complete function after -p and --path flags in command line - '-p': cmd.path_complete, - '--path': cmd.path_complete, - } + flag_dict = { + # Tab complete food items after -f and --food flags in command line + '-f': self.food_item_strs, + '--food': self.food_item_strs, + # Tab complete sport items after -s and --sport flags in command line + '-s': self.sport_item_strs, + '--sport': self.sport_item_strs, + # Tab complete using path_complete function after -p and --path flags in command line + '-p': cmd.path_complete, + '--path': cmd.path_complete, + } return cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) @@ -57,12 +53,11 @@ class BasicCompletionCommandSet(CommandSet): def complete_index_based(self, cmd: Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: """Completion function for do_index_based""" - index_dict = \ - { - 1: self.food_item_strs, # Tab complete food items at index 1 in command line - 2: self.sport_item_strs, # Tab complete sport items at index 2 in command line - 3: cmd.path_complete, # Tab complete using path_complete function at index 3 in command line - } + index_dict = { + 1: self.food_item_strs, # Tab complete food items at index 1 in command line + 2: self.sport_item_strs, # Tab complete sport items at index 2 in command line + 3: cmd.path_complete, # Tab complete using path_complete function at index 3 in command line + } return cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py index 579c0677..03bc2507 100644 --- a/examples/modular_commands/commandset_complex.py +++ b/examples/modular_commands/commandset_complex.py @@ -27,9 +27,8 @@ class CommandSetA(cmd2.CommandSet): def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): - self._cmd.poutput('Unknown: ' + ', '.join(['{}']*len(unknown)).format(*unknown)) - self._cmd.last_result = {'arg1': ns.arg1, - 'unknown': unknown} + self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown)) + self._cmd.last_result = {'arg1': ns.arg1, 'unknown': unknown} def help_cranberry(self): self._cmd.stdout.write('This command does diddly squat...\n') @@ -39,7 +38,7 @@ class CommandSetA(cmd2.CommandSet): def do_durian(self, args: List[str]): """Durian Command""" self._cmd.poutput('{} Arguments: '.format(len(args))) - self._cmd.poutput(', '.join(['{}']*len(args)).format(*args)) + self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args)) def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: return utils.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) diff --git a/examples/modular_commands_dynamic.py b/examples/modular_commands_dynamic.py index b2be5dd1..86a74d4b 100644 --- a/examples/modular_commands_dynamic.py +++ b/examples/modular_commands_dynamic.py @@ -10,6 +10,7 @@ on which CommandSets are loaded """ import argparse + import cmd2 from cmd2 import CommandSet, with_argparser, with_category, with_default_category diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index b698e00f..34837481 100644 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -7,12 +7,13 @@ with examples of how to integrate tab completion with argparse-based commands. import argparse from typing import Dict, Iterable, List, Optional -from cmd2 import Cmd, Cmd2ArgumentParser, CommandSet, CompletionItem, with_argparser -from cmd2.utils import CompletionError, basic_complete from modular_commands.commandset_basic import BasicCompletionCommandSet # noqa: F401 from modular_commands.commandset_complex import CommandSetA # noqa: F401 from modular_commands.commandset_custominit import CustomInitCommandSet # noqa: F401 +from cmd2 import Cmd, Cmd2ArgumentParser, CommandSet, CompletionItem, with_argparser +from cmd2.utils import CompletionError, basic_complete + # Data source for argparse.choices food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] @@ -34,12 +35,7 @@ def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[s def choices_completion_item() -> List[CompletionItem]: """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" - items = \ - { - 1: "My item", - 2: "Another item", - 3: "Yet another item" - } + items = {1: "My item", 2: "Another item", 3: "Yet another item"} return [CompletionItem(item_id, description) for item_id, description in items.items()] @@ -81,38 +77,48 @@ class WithCommandSets(Cmd): raise CompletionError("debug must be true") # Parser for example command - example_parser = Cmd2ArgumentParser(description="Command demonstrating tab completion with argparse\n" - "Notice even the flags of this command tab complete") + example_parser = Cmd2ArgumentParser( + description="Command demonstrating tab completion with argparse\n" "Notice even the flags of this command tab complete" + ) # Tab complete from a list using argparse choices. Set metavar if you don't # want the entire choices list showing in the usage text for this command. - example_parser.add_argument('--choices', choices=food_item_strs, metavar="CHOICE", - help="tab complete using choices") + example_parser.add_argument('--choices', choices=food_item_strs, metavar="CHOICE", help="tab complete using choices") # Tab complete from choices provided by a choices function and choices method - example_parser.add_argument('--choices_function', choices_function=choices_function, - help="tab complete using a choices_function") - example_parser.add_argument('--choices_method', choices_method=choices_method, - help="tab complete using a choices_method") + example_parser.add_argument( + '--choices_function', choices_function=choices_function, help="tab complete using a choices_function" + ) + example_parser.add_argument('--choices_method', choices_method=choices_method, help="tab complete using a choices_method") # Tab complete using a completer function and completer method - example_parser.add_argument('--completer_function', completer_function=completer_function, - help="tab complete using a completer_function") - example_parser.add_argument('--completer_method', completer_method=Cmd.path_complete, - help="tab complete using a completer_method") + example_parser.add_argument( + '--completer_function', completer_function=completer_function, help="tab complete using a completer_function" + ) + example_parser.add_argument( + '--completer_method', completer_method=Cmd.path_complete, help="tab complete using a completer_method" + ) # Demonstrate raising a CompletionError while tab completing - example_parser.add_argument('--completion_error', choices_method=choices_completion_error, - help="raise a CompletionError while tab completing if debug is False") + example_parser.add_argument( + '--completion_error', + choices_method=choices_completion_error, + help="raise a CompletionError while tab completing if debug is False", + ) # Demonstrate returning CompletionItems instead of strings - example_parser.add_argument('--completion_item', choices_function=choices_completion_item, metavar="ITEM_ID", - descriptive_header="Description", - help="demonstrate use of CompletionItems") + example_parser.add_argument( + '--completion_item', + choices_function=choices_completion_item, + metavar="ITEM_ID", + descriptive_header="Description", + help="demonstrate use of CompletionItems", + ) # Demonstrate use of arg_tokens dictionary - example_parser.add_argument('--arg_tokens', choices_function=choices_arg_tokens, - help="demonstrate use of arg_tokens dictionary") + example_parser.add_argument( + '--arg_tokens', choices_function=choices_arg_tokens, help="demonstrate use of arg_tokens dictionary" + ) @with_argparser(example_parser) def do_example(self, _: argparse.Namespace) -> None: diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index c1d499f0..d3c16198 100644 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -11,6 +11,7 @@ The `load` and `unload` command will load and unload the CommandSets. The availa subcommands to the `cut` command will change depending on which CommandSets are loaded. """ import argparse + import cmd2 from cmd2 import CommandSet, with_argparser, with_category, with_default_category diff --git a/examples/override_parser.py b/examples/override_parser.py index b6548388..d7d45b82 100755 --- a/examples/override_parser.py +++ b/examples/override_parser.py @@ -20,7 +20,8 @@ argparse.cmd2_parser_module = 'examples.custom_parser' if __name__ == '__main__': import sys + app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat') - app.self_in_py = True # Enable access to "self" within the py command - app.debug = True # Show traceback if/when an exception occurs + app.self_in_py = True # Enable access to "self" within the py command + app.debug = True # Show traceback if/when an exception occurs sys.exit(app.cmdloop()) diff --git a/examples/paged_output.py b/examples/paged_output.py index cba5c7c5..1c323c61 100755 --- a/examples/paged_output.py +++ b/examples/paged_output.py @@ -55,5 +55,6 @@ class PagedOutput(cmd2.Cmd): if __name__ == '__main__': import sys + app = PagedOutput() sys.exit(app.cmdloop()) diff --git a/examples/persistent_history.py b/examples/persistent_history.py index bc62cb14..330e3537 100755 --- a/examples/persistent_history.py +++ b/examples/persistent_history.py @@ -10,6 +10,7 @@ import cmd2 class Cmd2PersistentHistory(cmd2.Cmd): """Basic example of how to enable persistent readline history within your cmd2 app.""" + def __init__(self, hist_file): """Configure the app to load persistent history from a file (both readline and cmd2 history command affected). diff --git a/examples/pirate.py b/examples/pirate.py index a50f9a51..ba5bc7d5 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -15,6 +15,7 @@ from cmd2.constants import MULTILINE_TERMINATOR class Pirate(cmd2.Cmd): """A piratical example cmd2 application involving looting and drinking.""" + def __init__(self): """Initialize the base class as well as this one""" shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) @@ -40,7 +41,7 @@ class Pirate(cmd2.Cmd): def postcmd(self, stop, line): """Runs right before a command is about to return.""" if self.gold != self.initial_gold: - self.poutput('Now we gots {0} doubloons'.format(self.gold)) + self.poutput('Now we gots {} doubloons'.format(self.gold)) if self.gold < 0: self.poutput("Off to debtorrr's prison.") self.exit_code = -1 @@ -60,7 +61,7 @@ class Pirate(cmd2.Cmd): self.gold -= int(arg) except ValueError: if arg: - self.poutput('''What's "{0}"? I'll take rrrum.'''.format(arg)) + self.poutput('''What's "{}"? I'll take rrrum.'''.format(arg)) self.gold -= 1 def do_quit(self, arg): @@ -83,11 +84,12 @@ class Pirate(cmd2.Cmd): chant = ['yo'] + ['ho'] * args.ho separator = ', ' if args.commas else ' ' chant = separator.join(chant) - self.poutput('{0} and a bottle of {1}'.format(chant, args.beverage)) + self.poutput('{} and a bottle of {}'.format(chant, args.beverage)) if __name__ == '__main__': import sys + # Create an instance of the Pirate derived class and enter the REPL with cmdloop(). pirate = Pirate() sys_exit_code = pirate.cmdloop() diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py index a30e4c70..8784aa40 100755 --- a/examples/plumbum_colors.py +++ b/examples/plumbum_colors.py @@ -27,9 +27,10 @@ WARNING: This example requires the plumbum package, which isn't normally require """ import argparse +from plumbum.colors import bg, fg + import cmd2 from cmd2 import ansi -from plumbum.colors import bg, fg class FgColors(ansi.ColorBase): @@ -70,6 +71,7 @@ ansi.bg_lookup = get_bg class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" + def __init__(self): # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell super().__init__(use_ipython=True) @@ -97,7 +99,7 @@ class CmdLineApp(cmd2.Cmd): words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 6e4295d4..9465f697 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -118,5 +118,6 @@ class CmdLineApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = CmdLineApp() sys.exit(c.cmdloop()) diff --git a/examples/remove_builtin_commands.py b/examples/remove_builtin_commands.py index 4c1794d6..b0b19447 100755 --- a/examples/remove_builtin_commands.py +++ b/examples/remove_builtin_commands.py @@ -27,5 +27,6 @@ class RemoveBuiltinCommands(cmd2.Cmd): if __name__ == '__main__': import sys + app = RemoveBuiltinCommands() sys.exit(app.cmdloop()) diff --git a/examples/remove_settable.py b/examples/remove_settable.py index 6a2e4062..a7b87126 100755 --- a/examples/remove_settable.py +++ b/examples/remove_settable.py @@ -7,7 +7,6 @@ import cmd2 class MyApp(cmd2.Cmd): - def __init__(self): super().__init__() self.remove_settable('debug') @@ -15,5 +14,6 @@ class MyApp(cmd2.Cmd): if __name__ == '__main__': import sys + c = MyApp() sys.exit(c.cmdloop()) diff --git a/examples/scripts/arg_printer.py b/examples/scripts/arg_printer.py index f5f30c6d..924e269a 100755 --- a/examples/scripts/arg_printer.py +++ b/examples/scripts/arg_printer.py @@ -3,7 +3,6 @@ import os import sys -print("Running Python script {!r} which was called with {} arguments".format(os.path.basename(sys.argv[0]), - len(sys.argv) - 1)) +print("Running Python script {!r} which was called with {} arguments".format(os.path.basename(sys.argv[0]), len(sys.argv) - 1)) for i, arg in enumerate(sys.argv[1:]): print("arg {}: {!r}".format(i + 1, arg)) diff --git a/examples/subcommands.py b/examples/subcommands.py index dd69d97e..56c91b14 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -68,6 +68,7 @@ class SubcommandsExample(cmd2.Cmd): Example cmd2 application where we a base command which has a couple subcommands and the "sport" subcommand has tab completion enabled. """ + def __init__(self): super().__init__() @@ -114,5 +115,6 @@ class SubcommandsExample(cmd2.Cmd): if __name__ == '__main__': import sys + app = SubcommandsExample() sys.exit(app.cmdloop()) diff --git a/examples/table_creation.py b/examples/table_creation.py index 6325b200..e199afdc 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -11,6 +11,7 @@ from cmd2.table_creator import AlternatingTable, BorderedTable, Column, Horizont class DollarFormatter: """Example class to show that any object type can be passed as data to TableCreator and converted to a string""" + def __init__(self, val: float) -> None: self.val = val @@ -28,27 +29,28 @@ green = functools.partial(ansi.style, fg=ansi.fg.green) columns: List[Column] = list() columns.append(Column("Name", width=20)) columns.append(Column("Address", width=38)) -columns.append(Column("Income", width=14, - header_horiz_align=HorizontalAlignment.RIGHT, - data_horiz_align=HorizontalAlignment.RIGHT)) +columns.append( + Column("Income", width=14, header_horiz_align=HorizontalAlignment.RIGHT, data_horiz_align=HorizontalAlignment.RIGHT) +) # Table data which demonstrates handling of wrapping and text styles data_list: List[List[Any]] = list() -data_list.append(["Billy Smith", - "123 Sesame St.\n" - "Fake Town, USA 33445", DollarFormatter(100333.03)]) -data_list.append(["William Longfellow Marmaduke III", - "984 Really Long Street Name Which Will Wrap Nicely\n" - "Apt 22G\n" - "Pensacola, FL 32501", DollarFormatter(55135.22)]) -data_list.append(["James " + blue("Bluestone"), - bold_yellow("This address has line feeds,\n" - "text styles, and wrapping. ") + blue("Style is preserved across lines."), - DollarFormatter(300876.10)]) -data_list.append(["John Jones", - "9235 Highway 32\n" - + green("Greenville") + ", SC 29604", - DollarFormatter(82987.71)]) +data_list.append(["Billy Smith", "123 Sesame St.\n" "Fake Town, USA 33445", DollarFormatter(100333.03)]) +data_list.append( + [ + "William Longfellow Marmaduke III", + "984 Really Long Street Name Which Will Wrap Nicely\n" "Apt 22G\n" "Pensacola, FL 32501", + DollarFormatter(55135.22), + ] +) +data_list.append( + [ + "James " + blue("Bluestone"), + bold_yellow("This address has line feeds,\n" "text styles, and wrapping. ") + blue("Style is preserved across lines."), + DollarFormatter(300876.10), + ] +) +data_list.append(["John Jones", "9235 Highway 32\n" + green("Greenville") + ", SC 29604", DollarFormatter(82987.71)]) def ansi_print(text): diff --git a/examples/unicode_commands.py b/examples/unicode_commands.py index 0a7c5ac7..df02b028 100755 --- a/examples/unicode_commands.py +++ b/examples/unicode_commands.py @@ -16,7 +16,7 @@ class UnicodeApp(cmd2.Cmd): def do_πprint(self, _): """This command prints π to 5 decimal places.""" - self.poutput("π = {0:.6}".format(math.pi)) + self.poutput("π = {:.6}".format(math.pi)) def do_δ½ ε₯½(self, arg): """This command says hello in Chinese (Mandarin).""" @@ -3,23 +3,19 @@ import nox @nox.session(python=['3.7']) def docs(session): - session.install('sphinx', - 'sphinx-rtd-theme', - '.', - 'plugins/ext_test', - ) + session.install( + 'sphinx', 'sphinx-rtd-theme', '.', 'plugins/ext_test', + ) session.chdir('docs') tmpdir = session.create_tmp() - session.run('sphinx-build', '-a', '-W', '-T', '-b', 'html', - '-d', '{}/doctrees'.format(tmpdir), '.', '{}/html'.format(tmpdir)) + session.run( + 'sphinx-build', '-a', '-W', '-T', '-b', 'html', '-d', '{}/doctrees'.format(tmpdir), '.', '{}/html'.format(tmpdir) + ) @nox.session(python=['3.5.2', '3.5.3', '3.5', '3.6', '3.7', '3.8', '3.9']) -@nox.parametrize('plugin', [None, - 'ext_test', - 'template', - 'coverage']) +@nox.parametrize('plugin', [None, 'ext_test', 'template', 'coverage']) def tests(session, plugin): if plugin is None: session.install('invoke', './[test]') @@ -33,9 +29,6 @@ def tests(session, plugin): session.install('invoke', './', 'plugins/{}[test]'.format(plugin)) # cd into test directory to run other unit test - session.run('invoke', - 'plugin.{}.pytest'.format(plugin.replace('_', '-')), - '--junit', - '--no-pty', - '--append-cov', - ) + session.run( + 'invoke', 'plugin.{}.pytest'.format(plugin.replace('_', '-')), '--junit', '--no-pty', '--append-cov', + ) diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py index 21fd000b..2cc38807 100644 --- a/plugins/ext_test/cmd2_ext_test/__init__.py +++ b/plugins/ext_test/cmd2_ext_test/__init__.py @@ -19,6 +19,4 @@ except importlib_metadata.PackageNotFoundError: # pragma: no cover from .cmd2_ext_test import ExternalTestMixin -__all__ = [ - 'ExternalTestMixin' -] +__all__ = ['ExternalTestMixin'] diff --git a/plugins/ext_test/examples/example.py b/plugins/ext_test/examples/example.py index d7f0c762..7dbb6677 100644 --- a/plugins/ext_test/examples/example.py +++ b/plugins/ext_test/examples/example.py @@ -1,13 +1,15 @@ # # coding=utf-8 # import cmd2 +import cmd2_ext_test + import cmd2 import cmd2.py_bridge -import cmd2_ext_test class Example(cmd2.Cmd): """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, **kwargs) diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index 3384527c..4bfe1b79 100644 --- a/plugins/ext_test/setup.py +++ b/plugins/ext_test/setup.py @@ -20,23 +20,18 @@ setuptools.setup( # 'relative_to': __file__, # 'git_describe_command': 'git describe --dirty --tags --long --match plugin-ext-test*' # }, - description='External test plugin for cmd2. Allows for external invocation of commands as if from a cmd2 pyscript', long_description=long_description, long_description_content_type='text/markdown', keywords='cmd2 test plugin', - author='Eric Lin', author_email='anselor@gmail.com', url='https://github.com/python-cmd2/cmd2/tree/master/plugins/ext_test', license='MIT', - packages=['cmd2_ext_test'], - python_requires='>=3.5', install_requires=['cmd2 >= 0.9.4, <=2'], setup_requires=['setuptools_scm >= 3.0'], - classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', @@ -51,17 +46,10 @@ setuptools.setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', ], - # dependencies for development and testing # $ pip install -e .[dev] extras_require={ - 'test': [ - 'codecov', - 'coverage', - 'pytest', - 'pytest-cov', - ], - 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', - 'pylint', 'invoke', 'wheel', 'twine'] + 'test': ['codecov', 'coverage', 'pytest', 'pytest-cov',], + 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', 'pylint', 'invoke', 'wheel', 'twine'], }, ) diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py index f23faa69..757dfe79 100644 --- a/plugins/ext_test/tasks.py +++ b/plugins/ext_test/tasks.py @@ -57,7 +57,7 @@ def pytest(context, junit=False, pty=True, append_cov=False): command_str += ' --cov-append' if junit: command_str += ' --junitxml=junit/test-results.xml' - command_str += ' ' + str((TASK_ROOT/'tests').relative_to(ROOT_PATH)) + command_str += ' ' + str((TASK_ROOT / 'tests').relative_to(ROOT_PATH)) context.run(command_str, pty=pty) @@ -71,8 +71,8 @@ def pytest_clean(context): with context.cd(TASK_ROOT_STR): dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] rmrf(dirs) - - + + namespace_clean.add_task(pytest_clean, 'pytest') @@ -140,6 +140,8 @@ namespace_clean.add_task(dist_clean, 'dist') # # make a dummy clean task which runs all the tasks in the clean namespace clean_tasks = list(namespace_clean.tasks.values()) + + @invoke.task(pre=list(namespace_clean.tasks.values()), default=True) def clean_all(context): """Run all clean tasks""" @@ -195,8 +197,10 @@ namespace.add_task(pypi_test) def flake8(context): """Run flake8 linter and tool for style guide enforcement""" with context.cd(TASK_ROOT_STR): - context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics " - "--exclude=.git,__pycache__,.tox,.nox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") + context.run( + "flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics " + "--exclude=.git,__pycache__,.tox,.nox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov" + ) namespace.add_task(flake8) diff --git a/plugins/ext_test/tests/test_ext_test.py b/plugins/ext_test/tests/test_ext_test.py index b1ba1b7d..52441340 100644 --- a/plugins/ext_test/tests/test_ext_test.py +++ b/plugins/ext_test/tests/test_ext_test.py @@ -1,9 +1,9 @@ # # coding=utf-8 +import cmd2_ext_test import pytest -import cmd2_ext_test from cmd2 import CommandResult, cmd2 ###### @@ -17,6 +17,7 @@ OUT_MSG = 'this is the something command' class ExampleApp(cmd2.Cmd): """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, **kwargs) @@ -28,11 +29,13 @@ class ExampleApp(cmd2.Cmd): # Define a tester class that brings in the external test mixin + class ExampleTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): def __init__(self, *args, **kwargs): # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, **kwargs) + # # You can't use a fixture to instantiate your app if you want to use # to use the capsys fixture to capture the output. cmd2.Cmd sets @@ -59,6 +62,7 @@ def example_app(): # ##### + def test_something(example_app): # load our fixture # execute a command diff --git a/plugins/tasks.py b/plugins/tasks.py index 1a70e4f2..7d347454 100644 --- a/plugins/tasks.py +++ b/plugins/tasks.py @@ -14,9 +14,7 @@ from plugins.ext_test import tasks as ext_test_tasks from plugins.template import tasks as template_tasks # create namespaces -namespace = invoke.Collection(ext_test=ext_test_tasks, - template=template_tasks, - ) +namespace = invoke.Collection(ext_test=ext_test_tasks, template=template_tasks,) namespace_clean = invoke.Collection('clean') namespace.add_collection(namespace_clean, 'clean') diff --git a/plugins/template/cmd2_myplugin/myplugin.py b/plugins/template/cmd2_myplugin/myplugin.py index 816198b0..b89bcd72 100644 --- a/plugins/template/cmd2_myplugin/myplugin.py +++ b/plugins/template/cmd2_myplugin/myplugin.py @@ -60,10 +60,7 @@ class MyPluginMixin(_Base): """Method to be called after the command loop finishes""" self.poutput("postloop hook") - def cmd2_myplugin_postparsing_hook( - self, - data: cmd2.plugin.PostparsingData - ) -> cmd2.plugin.PostparsingData: + def cmd2_myplugin_postparsing_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: """Method to be called after parsing user input, but before running the command""" self.poutput('in postparsing hook') return data diff --git a/plugins/template/examples/example.py b/plugins/template/examples/example.py index 2c9b8e5c..b071b5f8 100644 --- a/plugins/template/examples/example.py +++ b/plugins/template/examples/example.py @@ -1,12 +1,14 @@ # # coding=utf-8 -import cmd2 import cmd2_myplugin +import cmd2 + class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, **kwargs) diff --git a/plugins/template/setup.py b/plugins/template/setup.py index cb1dfd8e..e0f458e4 100644 --- a/plugins/template/setup.py +++ b/plugins/template/setup.py @@ -15,23 +15,18 @@ setuptools.setup( name='cmd2-myplugin', # use_scm_version=True, # use_scm_version doesn't work if setup.py isn't in the repository root version='1.0.1', - description='A template used to build plugins for cmd2', long_description=long_description, long_description_content_type='text/markdown', keywords='cmd2 plugin', - author='Kotfu', author_email='kotfu@kotfu.net', url='https://github.com/python-cmd2/cmd2-plugin-template', license='MIT', - packages=['cmd2_myplugin'], - python_requires='>=3.4', install_requires=['cmd2 >= 0.9.4, <=2'], setup_requires=['setuptools_scm'], - classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', @@ -44,17 +39,10 @@ setuptools.setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - # dependencies for development and testing # $ pip install -e .[dev] extras_require={ - 'test': [ - 'codecov', - 'coverage', - 'pytest', - 'pytest-cov', - ], - 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', - 'pylint', 'invoke', 'wheel', 'twine'] + 'test': ['codecov', 'coverage', 'pytest', 'pytest-cov',], + 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', 'pylint', 'invoke', 'wheel', 'twine'], }, ) diff --git a/plugins/template/tests/test_myplugin.py b/plugins/template/tests/test_myplugin.py index d61181a6..dc3677da 100644 --- a/plugins/template/tests/test_myplugin.py +++ b/plugins/template/tests/test_myplugin.py @@ -2,6 +2,7 @@ # coding=utf-8 import cmd2_myplugin + from cmd2 import cmd2 ###### @@ -13,6 +14,7 @@ from cmd2 import cmd2 class MyApp(cmd2_myplugin.MyPluginMixin, cmd2.Cmd): """Simple subclass of cmd2.Cmd with our SayMixin plugin included.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -20,6 +22,7 @@ class MyApp(cmd2_myplugin.MyPluginMixin, cmd2.Cmd): def do_empty(self, args): self.poutput("running the empty command") + # # You can't use a fixture to instantiate your app if you want to use # to use the capsys fixture to capture the output. cmd2.Cmd sets @@ -43,6 +46,7 @@ def init_app(): # ##### + def test_say(capsys): # call our initialization function instead of using a fixture app = init_app() @@ -3,13 +3,31 @@ testpaths = tests [flake8] -exclude = .git,.idea,.pytest_cache,.tox,.nox,.venv,.vscode,build,cmd2.egg-info,dist,htmlcov,__pycache__,*.egg -max-line-length = 127 +count = True +ignore = E252,W503,E231,E203 # E231 can be removed once black is fixed. max-complexity = 26 +max-line-length = 127 +show-source = True +statistics = True +exclude = + .git + __pycache__ + .tox + .nox + .eggs + *.eggs, + .venv, + .idea, + .pytest_cache, + .vscode, + build, + dist, + htmlcov [isort] -line_length=127 -skip=cmd2/__init__.py +line_length = 127 +skip = cmd2/__init__.py +profile = black multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 @@ -12,8 +12,12 @@ DESCRIPTION = "cmd2 - quickly build feature-rich and user-friendly interactive c with codecs.open('README.md', encoding='utf8') as f: LONG_DESCRIPTION = f.read() -CLASSIFIERS = list(filter(None, map(str.strip, -""" +CLASSIFIERS = list( + filter( + None, + map( + str.strip, + """ Development Status :: 5 - Production/Stable Environment :: Console Operating System :: OS Independent @@ -29,7 +33,10 @@ Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules -""".splitlines()))) # noqa: E128 +""".splitlines(), + ), + ) +) # noqa: E128 SETUP_REQUIRES = ['setuptools >= 34.4', 'setuptools_scm >= 3.0'] @@ -55,24 +62,27 @@ EXTRAS_REQUIRE = { 'pytest-mock', ], # development only dependencies: install with 'pip install -e .[dev]' - 'dev': ["mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module - "pytest>=4.6", - 'codecov', - 'pytest-cov', - 'pytest-mock', - "nox==2019.11.9 ; python_version=='3.5.2'", - "nox ; python_version>'3.5.2'", - 'flake8', - 'sphinx', 'sphinx-rtd-theme', 'sphinx-autobuild', 'doc8', - 'invoke', 'twine>=1.11', - ] + 'dev': [ + "mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module + "pytest>=4.6", + 'codecov', + 'pytest-cov', + 'pytest-mock', + "nox==2019.11.9 ; python_version=='3.5.2'", + "nox ; python_version>'3.5.2'", + 'flake8', + 'sphinx', + 'sphinx-rtd-theme', + 'sphinx-autobuild', + 'doc8', + 'invoke', + 'twine>=1.11', + ], } setup( name="cmd2", - use_scm_version={ - 'git_describe_command': 'git describe --dirty --tags --long --exclude plugin-*' - }, + use_scm_version={'git_describe_command': 'git describe --dirty --tags --long --exclude plugin-*'}, description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', @@ -70,7 +70,7 @@ def pytest(context, junit=False, pty=True, base=False, isolated=False): tests_cmd = command_str + ' tests' context.run(tests_cmd, pty=pty) if isolated: - for root, dirnames, _ in os.walk(str(TASK_ROOT/'tests_isolated')): + for root, dirnames, _ in os.walk(str(TASK_ROOT / 'tests_isolated')): for dir in dirnames: if dir.startswith('test_'): context.run(command_str + ' tests_isolated/' + dir) @@ -83,7 +83,7 @@ namespace.add_task(pytest) def pytest_clean(context): """Remove pytest cache and code coverage files and directories""" # pylint: disable=unused-argument - with context.cd(str(TASK_ROOT/'tests')): + with context.cd(str(TASK_ROOT / 'tests')): dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] rmrf(dirs) rmrf(dirs) @@ -132,7 +132,7 @@ namespace_clean.add_task(nox_clean, 'nox') ##### DOCS_SRCDIR = 'docs' DOCS_BUILDDIR = os.path.join('docs', '_build') -SPHINX_OPTS = '-nvWT' # Be nitpicky, verbose, and treat warnings as errors +SPHINX_OPTS = '-nvWT' # Be nitpicky, verbose, and treat warnings as errors @invoke.task() @@ -150,7 +150,7 @@ namespace.add_task(docs) def doc8(context): """Check documentation with doc8""" with context.cd(TASK_ROOT_STR): - context.run('doc8 docs --ignore-path docs/_build') + context.run('doc8 docs') namespace.add_task(doc8) @@ -170,7 +170,7 @@ namespace_clean.add_task(docs_clean, name='docs') @invoke.task() def linkcheck(context): """Check external links in Sphinx documentation for integrity.""" - with context.cd(str(TASK_ROOT/'docs')): + with context.cd(str(TASK_ROOT / 'docs')): context.run('make linkcheck', pty=True) @@ -347,8 +347,7 @@ namespace.add_task(pypi_test) def flake8(context): """Run flake8 linter and tool for style guide enforcement""" with context.cd(TASK_ROOT_STR): - context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics " - "--exclude=.git,__pycache__,.tox,.nox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov") + context.run("flake8") namespace.add_task(flake8) diff --git a/tests/conftest.py b/tests/conftest.py index 73080b5c..1116539d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,9 +25,9 @@ except ImportError: pass -def verify_help_text(cmd2_app: cmd2.Cmd, - help_output: Union[str, List[str]], - verbose_strings: Optional[List[str]] = None) -> None: +def verify_help_text( + cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None +) -> None: """This function verifies that all expected commands are present in the help text. :param cmd2_app: instance of cmd2.Cmd @@ -158,12 +158,7 @@ def base_app(): # These are odd file names for testing quoting of them -odd_file_names = [ - 'nothingweird', - 'has spaces', - '"is_double_quoted"', - "'is_single_quoted'" -] +odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: @@ -182,6 +177,7 @@ def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Opti Matches are stored in app.completion_matches These matches also have been sorted by complete() """ + def get_line(): return line diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py index 7f02bb78..b88ba5a5 100644 --- a/tests/pyscript/recursive.py +++ b/tests/pyscript/recursive.py @@ -8,5 +8,5 @@ import os import sys app.cmd_echo = True -my_dir = (os.path.dirname(os.path.realpath(sys.argv[0]))) +my_dir = os.path.dirname(os.path.realpath(sys.argv[0])) app('run_pyscript {}'.format(os.path.join(my_dir, 'stop.py'))) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 4a28b1a0..8051b248 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -65,11 +65,19 @@ def test_style_multi(): base_str = HELLO_WORLD fg_color = 'blue' bg_color = 'green' - ansi_str = (ansi.fg[fg_color].value + ansi.bg[bg_color].value + - ansi.INTENSITY_BRIGHT + ansi.INTENSITY_DIM + ansi.UNDERLINE_ENABLE + - base_str + - ansi.FG_RESET + ansi.BG_RESET + - ansi.INTENSITY_NORMAL + ansi.INTENSITY_NORMAL + ansi.UNDERLINE_DISABLE) + ansi_str = ( + ansi.fg[fg_color].value + + ansi.bg[bg_color].value + + ansi.INTENSITY_BRIGHT + + ansi.INTENSITY_DIM + + ansi.UNDERLINE_ENABLE + + base_str + + ansi.FG_RESET + + ansi.BG_RESET + + ansi.INTENSITY_NORMAL + + ansi.INTENSITY_NORMAL + + ansi.UNDERLINE_DISABLE + ) assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, dim=True, underline=True) == ansi_str @@ -110,14 +118,23 @@ def test_set_title_str(): assert ansi.set_title_str(title) == OSC + '2;' + title + BEL -@pytest.mark.parametrize('cols, prompt, line, cursor, msg, expected', [ - (127, '(Cmd) ', 'help his', 12, ansi.style('Hello World!', fg='magenta'), '\x1b[2K\r\x1b[35mHello World!\x1b[39m'), - (127, '\n(Cmd) ', 'help ', 5, 'foo', '\x1b[2K\x1b[1A\x1b[2K\rfoo'), - (10, '(Cmd) ', 'help history of the american republic', 4, 'boo', '\x1b[3B\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\rboo') -]) +@pytest.mark.parametrize( + 'cols, prompt, line, cursor, msg, expected', + [ + (127, '(Cmd) ', 'help his', 12, ansi.style('Hello World!', fg='magenta'), '\x1b[2K\r\x1b[35mHello World!\x1b[39m'), + (127, '\n(Cmd) ', 'help ', 5, 'foo', '\x1b[2K\x1b[1A\x1b[2K\rfoo'), + ( + 10, + '(Cmd) ', + 'help history of the american republic', + 4, + 'boo', + '\x1b[3B\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\rboo', + ), + ], +) def test_async_alert_str(cols, prompt, line, cursor, msg, expected): - alert_str = ansi.async_alert_str(terminal_columns=cols, prompt=prompt, line=line, cursor_offset=cursor, - alert_msg=msg) + alert_str = ansi.async_alert_str(terminal_columns=cols, prompt=prompt, line=line, cursor_offset=cursor, alert_msg=msg) assert alert_str == expected @@ -127,19 +144,23 @@ def test_cast_color_as_str(): def test_color_str_building(): - from cmd2.ansi import fg, bg + from cmd2.ansi import bg, fg + assert fg.blue + "hello" == fg.blue.value + "hello" assert bg.blue + "hello" == bg.blue.value + "hello" assert fg.blue + "hello" + fg.reset == fg.blue.value + "hello" + fg.reset.value assert bg.blue + "hello" + bg.reset == bg.blue.value + "hello" + bg.reset.value - assert fg.blue + bg.white + "hello" + fg.reset + bg.reset == \ - fg.blue.value + bg.white.value + "hello" + fg.reset.value + bg.reset.value + assert ( + fg.blue + bg.white + "hello" + fg.reset + bg.reset + == fg.blue.value + bg.white.value + "hello" + fg.reset.value + bg.reset.value + ) def test_color_nonunique_values(): class Matching(ansi.ColorBase): magenta = ansi.fg_lookup('magenta') purple = ansi.fg_lookup('magenta') + assert sorted(Matching.colors()) == ['magenta', 'purple'] diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 7059e9d3..e91b4dba 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -49,7 +49,7 @@ class ArgparseApp(cmd2.Cmd): if word is None: word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -101,7 +101,7 @@ class ArgparseApp(cmd2.Cmd): if word is None: word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = '{}{}ay'.format(word[1:], word[0]) if args.shout: word = word.upper() words.append(word) @@ -308,8 +308,9 @@ class SubcommandApp(cmd2.Cmd): helpless_subcmd_parser = cmd2.Cmd2ArgumentParser(add_help=False, description="A subcommand with no help") - @cmd2.as_subcommand_to('test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser, - help=helpless_subcmd_parser.description.lower()) + @cmd2.as_subcommand_to( + 'test_subcmd_decorator', 'helpless_subcmd', helpless_subcmd_parser, help=helpless_subcmd_parser.description.lower() + ) def helpless_subcmd_func(self, args: argparse.Namespace): # Make sure vars(Namespace) works. The way we originally added cmd2_hander to it resulted in a RecursionError. self.poutput(vars(args)) @@ -425,6 +426,7 @@ def test_subcmd_decorator(subcommand_app): def test_unittest_mock(): from unittest import mock + from cmd2 import CommandSetRegistrationError with mock.patch.object(ArgparseApp, 'namespace_provider'): @@ -449,12 +451,7 @@ def test_pytest_mock_invalid(mocker): app = ArgparseApp() -@pytest.mark.parametrize('spec_param', [ - {'spec': True}, - {'spec_set': True}, - {'autospec': True}, -]) +@pytest.mark.parametrize('spec_param', [{'spec': True}, {'spec_set': True}, {'autospec': True},]) def test_pytest_mock_valid(mocker, spec_param): mocker.patch.object(ArgparseApp, 'namespace_provider', **spec_param) app = ArgparseApp() - diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index dd86163b..3898de85 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -12,6 +12,7 @@ import pytest import cmd2 from cmd2 import Cmd2ArgumentParser, CompletionItem, with_argparser from cmd2.utils import CompletionError, StdSim, basic_complete + from .conftest import complete_tester, run_cmd # Lists used in our tests (there is a mix of sorted and unsorted on purpose) @@ -48,8 +49,7 @@ def choices_takes_arg_tokens(arg_tokens: argparse.Namespace) -> List[str]: return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] -def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, - arg_tokens: argparse.Namespace) -> List[str]: +def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, arg_tokens: argparse.Namespace) -> List[str]: """Completer function that receives arg_tokens from ArgparseCompleter""" match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] return basic_complete(text, line, begidx, endidx, match_against) @@ -58,6 +58,7 @@ def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, # noinspection PyMethodMayBeStatic,PyUnusedLocal,PyProtectedMember class ArgparseCompleterTester(cmd2.Cmd): """Cmd2 app that exercises ArgparseCompleter class""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -129,34 +130,47 @@ class ArgparseCompleterTester(cmd2.Cmd): choices_parser = Cmd2ArgumentParser() # Flag args for choices command. Include string and non-string arg types. - choices_parser.add_argument("-l", "--list", help="a flag populated with a choices list", - choices=static_choices_list) - choices_parser.add_argument("-f", "--function", help="a flag populated with a choices function", - choices_function=choices_function) - choices_parser.add_argument("-m", "--method", help="a flag populated with a choices method", - choices_method=choices_method) - choices_parser.add_argument('-d', "--desc_header", help='this arg has a descriptive header', - choices_method=completion_item_method, - descriptive_header=CUSTOM_DESC_HEADER) - choices_parser.add_argument('-n', "--no_header", help='this arg has no descriptive header', - choices_method=completion_item_method, metavar=STR_METAVAR) - choices_parser.add_argument('-t', "--tuple_metavar", help='this arg has tuple for a metavar', - choices_method=completion_item_method, metavar=TUPLE_METAVAR, - nargs=argparse.ONE_OR_MORE) - choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', - choices=int_choices) + choices_parser.add_argument("-l", "--list", help="a flag populated with a choices list", choices=static_choices_list) + choices_parser.add_argument( + "-f", "--function", help="a flag populated with a choices function", choices_function=choices_function + ) + choices_parser.add_argument("-m", "--method", help="a flag populated with a choices method", choices_method=choices_method) + choices_parser.add_argument( + '-d', + "--desc_header", + help='this arg has a descriptive header', + choices_method=completion_item_method, + descriptive_header=CUSTOM_DESC_HEADER, + ) + choices_parser.add_argument( + '-n', + "--no_header", + help='this arg has no descriptive header', + choices_method=completion_item_method, + metavar=STR_METAVAR, + ) + choices_parser.add_argument( + '-t', + "--tuple_metavar", + help='this arg has tuple for a metavar', + choices_method=completion_item_method, + metavar=TUPLE_METAVAR, + nargs=argparse.ONE_OR_MORE, + ) + choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', choices=int_choices) # Positional args for choices command - choices_parser.add_argument("list_pos", help="a positional populated with a choices list", - choices=static_choices_list) - choices_parser.add_argument("function_pos", help="a positional populated with a choices function", - choices_function=choices_function) - choices_parser.add_argument("method_pos", help="a positional populated with a choices method", - choices_method=choices_method) - choices_parser.add_argument('non_negative_int', type=int, help='a positional with non-negative int choices', - choices=non_negative_int_choices) - choices_parser.add_argument('empty_choices', help='a positional with empty choices', - choices=[]) + choices_parser.add_argument("list_pos", help="a positional populated with a choices list", choices=static_choices_list) + choices_parser.add_argument( + "function_pos", help="a positional populated with a choices function", choices_function=choices_function + ) + choices_parser.add_argument( + "method_pos", help="a positional populated with a choices method", choices_method=choices_method + ) + choices_parser.add_argument( + 'non_negative_int', type=int, help='a positional with non-negative int choices', choices=non_negative_int_choices + ) + choices_parser.add_argument('empty_choices', help='a positional with empty choices', choices=[]) @with_argparser(choices_parser) def do_choices(self, args: argparse.Namespace) -> None: @@ -172,16 +186,18 @@ class ArgparseCompleterTester(cmd2.Cmd): completer_parser = Cmd2ArgumentParser() # Flag args for completer command - completer_parser.add_argument("-f", "--function", help="a flag using a completer function", - completer_function=completer_function) - completer_parser.add_argument("-m", "--method", help="a flag using a completer method", - completer_method=completer_method) + completer_parser.add_argument( + "-f", "--function", help="a flag using a completer function", completer_function=completer_function + ) + completer_parser.add_argument("-m", "--method", help="a flag using a completer method", completer_method=completer_method) # Positional args for completer command - completer_parser.add_argument("function_pos", help="a positional using a completer function", - completer_function=completer_function) - completer_parser.add_argument("method_pos", help="a positional using a completer method", - completer_method=completer_method) + completer_parser.add_argument( + "function_pos", help="a positional using a completer function", completer_function=completer_function + ) + completer_parser.add_argument( + "method_pos", help="a positional using a completer method", completer_method=completer_method + ) @with_argparser(completer_parser) def do_completer(self, args: argparse.Namespace) -> None: @@ -193,22 +209,23 @@ class ArgparseCompleterTester(cmd2.Cmd): nargs_parser = Cmd2ArgumentParser() # Flag args for nargs command - nargs_parser.add_argument("--set_value", help="a flag with a set value for nargs", nargs=2, - choices=set_value_choices) - nargs_parser.add_argument("--one_or_more", help="a flag wanting one or more args", nargs=argparse.ONE_OR_MORE, - choices=one_or_more_choices) - nargs_parser.add_argument("--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL, - choices=optional_choices) + nargs_parser.add_argument("--set_value", help="a flag with a set value for nargs", nargs=2, choices=set_value_choices) + nargs_parser.add_argument( + "--one_or_more", help="a flag wanting one or more args", nargs=argparse.ONE_OR_MORE, choices=one_or_more_choices + ) + nargs_parser.add_argument( + "--optional", help="a flag with an optional value", nargs=argparse.OPTIONAL, choices=optional_choices + ) # noinspection PyTypeChecker - nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2), - choices=range_choices) - nargs_parser.add_argument("--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER, - choices=remainder_choices) + nargs_parser.add_argument("--range", help="a flag with nargs range", nargs=(1, 2), choices=range_choices) + nargs_parser.add_argument( + "--remainder", help="a flag wanting remaining", nargs=argparse.REMAINDER, choices=remainder_choices + ) - nargs_parser.add_argument("normal_pos", help="a remainder positional", nargs=2, - choices=positional_choices) - nargs_parser.add_argument("remainder_pos", help="a remainder positional", nargs=argparse.REMAINDER, - choices=remainder_choices) + nargs_parser.add_argument("normal_pos", help="a remainder positional", nargs=2, choices=positional_choices) + nargs_parser.add_argument( + "remainder_pos", help="a remainder positional", nargs=argparse.REMAINDER, choices=remainder_choices + ) @with_argparser(nargs_parser) def do_nargs(self, args: argparse.Namespace) -> None: @@ -241,10 +258,8 @@ class ArgparseCompleterTester(cmd2.Cmd): raise CompletionError('choice broke something') comp_error_parser = Cmd2ArgumentParser() - comp_error_parser.add_argument('completer', help='positional arg', - completer_method=completer_raise_error) - comp_error_parser.add_argument('--choice', help='flag arg', - choices_method=choice_raise_error) + comp_error_parser.add_argument('completer', help='positional arg', completer_method=completer_raise_error) + comp_error_parser.add_argument('--choice', help='flag arg', choices_method=choice_raise_error) @with_argparser(comp_error_parser) def do_raise_completion_error(self, args: argparse.Namespace) -> None: @@ -294,12 +309,7 @@ def ac_app(): return app -@pytest.mark.parametrize('command', [ - 'music', - 'music create', - 'music create rock', - 'music create jazz' -]) +@pytest.mark.parametrize('command', ['music', 'music create', 'music create rock', 'music create jazz']) def test_help(ac_app, command): out1, err1 = run_cmd(ac_app, '{} -h'.format(command)) out2, err2 = run_cmd(ac_app, 'help {}'.format(command)) @@ -314,16 +324,19 @@ def test_bad_subcommand_help(ac_app): assert out1 == out2 -@pytest.mark.parametrize('command, text, completions', [ - ('', 'mus', ['music ']), - ('music', 'cre', ['create ']), - ('music', 'creab', []), - ('music create', '', ['jazz', 'rock']), - ('music crea', 'jazz', []), - ('music create', 'foo', []), - ('fake create', '', []), - ('music fake', '', []) -]) +@pytest.mark.parametrize( + 'command, text, completions', + [ + ('', 'mus', ['music ']), + ('music', 'cre', ['create ']), + ('music', 'creab', []), + ('music create', '', ['jazz', 'rock']), + ('music crea', 'jazz', []), + ('music create', 'foo', []), + ('fake create', '', []), + ('music fake', '', []), + ], +) def test_complete_help(ac_app, command, text, completions): line = 'help {} {}'.format(command, text) endidx = len(line) @@ -338,12 +351,10 @@ def test_complete_help(ac_app, command, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('subcommand, text, completions', [ - ('create', '', ['jazz', 'rock']), - ('create', 'ja', ['jazz ']), - ('create', 'foo', []), - ('creab', 'ja', []) -]) +@pytest.mark.parametrize( + 'subcommand, text, completions', + [('create', '', ['jazz', 'rock']), ('create', 'ja', ['jazz ']), ('create', 'foo', []), ('creab', 'ja', [])], +) def test_subcommand_completions(ac_app, subcommand, text, completions): line = 'music {} {}'.format(subcommand, text) endidx = len(line) @@ -358,64 +369,132 @@ def test_subcommand_completions(ac_app, subcommand, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, completion_matches, display_matches', [ - # Complete all flags (suppressed will not show) - ('flag', '-', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--normal_flag', - '--remainder_flag', '--required_flag', '-a', '-c', '-h', '-n', '-o', '-q', '-r'], - ['-q, --required_flag', '[-o, --append_const_flag]', '[-a, --append_flag]', '[-c, --count_flag]', '[-h, --help]', - '[-n, --normal_flag]', '[-r, --remainder_flag]']), - ('flag', '--', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', - '--normal_flag', '--remainder_flag', '--required_flag'], - ['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', - '[--normal_flag]', '[--remainder_flag]']), - - # Complete individual flag - ('flag', '-n', ['-n '], ['[-n]']), - ('flag', '--n', ['--normal_flag '], ['[--normal_flag]']), - - # No flags should complete until current flag has its args - ('flag --append_flag', '-', [], []), - - # Complete REMAINDER flag name - ('flag', '-r', ['-r '], ['[-r]']), - ('flag', '--rem', ['--remainder_flag '], ['[--remainder_flag]']), - - # No flags after a REMAINDER should complete - ('flag -r value', '-', [], []), - ('flag --remainder_flag value', '--', [], []), - - # Suppressed flag should not complete - ('flag', '-s', [], []), - ('flag', '--s', [], []), - - # A used flag should not show in completions - ('flag -n', '--', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag', '--required_flag'], - ['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', '[--remainder_flag]']), - - # Flags with actions set to append, append_const, and count will always show even if they've been used - ('flag --append_const_flag -c --append_flag value', '--', - ['--append_const_flag', '--append_flag', '--count_flag', '--help', - '--normal_flag', '--remainder_flag', '--required_flag'], - ['--required_flag', '[--append_const_flag]', '[--append_flag]', '[--count_flag]', '[--help]', - '[--normal_flag]', '[--remainder_flag]']), - - # Non-default flag prefix character (+) - ('plus_flag', '+', - ['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'], - ['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]']), - ('plus_flag', '++', - ['++help', '++normal_flag', '++required_flag'], - ['++required_flag', '[++help]', '[++normal_flag]']), - - # Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags - ('flag --', '--', [], []), - ('flag --help --', '--', [], []), - ('plus_flag --', '++', [], []), - ('plus_flag ++help --', '++', [], []) -]) +@pytest.mark.parametrize( + 'command_and_args, text, completion_matches, display_matches', + [ + # Complete all flags (suppressed will not show) + ( + 'flag', + '-', + [ + '--append_const_flag', + '--append_flag', + '--count_flag', + '--help', + '--normal_flag', + '--remainder_flag', + '--required_flag', + '-a', + '-c', + '-h', + '-n', + '-o', + '-q', + '-r', + ], + [ + '-q, --required_flag', + '[-o, --append_const_flag]', + '[-a, --append_flag]', + '[-c, --count_flag]', + '[-h, --help]', + '[-n, --normal_flag]', + '[-r, --remainder_flag]', + ], + ), + ( + 'flag', + '--', + [ + '--append_const_flag', + '--append_flag', + '--count_flag', + '--help', + '--normal_flag', + '--remainder_flag', + '--required_flag', + ], + [ + '--required_flag', + '[--append_const_flag]', + '[--append_flag]', + '[--count_flag]', + '[--help]', + '[--normal_flag]', + '[--remainder_flag]', + ], + ), + # Complete individual flag + ('flag', '-n', ['-n '], ['[-n]']), + ('flag', '--n', ['--normal_flag '], ['[--normal_flag]']), + # No flags should complete until current flag has its args + ('flag --append_flag', '-', [], []), + # Complete REMAINDER flag name + ('flag', '-r', ['-r '], ['[-r]']), + ('flag', '--rem', ['--remainder_flag '], ['[--remainder_flag]']), + # No flags after a REMAINDER should complete + ('flag -r value', '-', [], []), + ('flag --remainder_flag value', '--', [], []), + # Suppressed flag should not complete + ('flag', '-s', [], []), + ('flag', '--s', [], []), + # A used flag should not show in completions + ( + 'flag -n', + '--', + ['--append_const_flag', '--append_flag', '--count_flag', '--help', '--remainder_flag', '--required_flag'], + [ + '--required_flag', + '[--append_const_flag]', + '[--append_flag]', + '[--count_flag]', + '[--help]', + '[--remainder_flag]', + ], + ), + # Flags with actions set to append, append_const, and count will always show even if they've been used + ( + 'flag --append_const_flag -c --append_flag value', + '--', + [ + '--append_const_flag', + '--append_flag', + '--count_flag', + '--help', + '--normal_flag', + '--remainder_flag', + '--required_flag', + ], + [ + '--required_flag', + '[--append_const_flag]', + '[--append_flag]', + '[--count_flag]', + '[--help]', + '[--normal_flag]', + '[--remainder_flag]', + ], + ), + # Non-default flag prefix character (+) + ( + 'plus_flag', + '+', + ['++help', '++normal_flag', '+h', '+n', '+q', '++required_flag'], + ['+q, ++required_flag', '[+h, ++help]', '[+n, ++normal_flag]'], + ), + ( + 'plus_flag', + '++', + ['++help', '++normal_flag', '++required_flag'], + ['++required_flag', '[++help]', '[++normal_flag]'], + ), + # Flag completion should not occur after '--' since that tells argparse all remaining arguments are non-flags + ('flag --', '--', [], []), + ('flag --help --', '--', [], []), + ('plus_flag --', '++', [], []), + ('plus_flag ++help --', '++', [], []), + ], +) def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matches, display_matches): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -427,22 +506,26 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matc else: assert first_match is None - assert (ac_app.completion_matches == sorted(completion_matches, key=ac_app.default_sort_key) and - ac_app.display_matches == sorted(display_matches, key=ac_app.default_sort_key)) - - -@pytest.mark.parametrize('flag, text, completions', [ - ('-l', '', static_choices_list), - ('--list', 's', ['static', 'stop']), - ('-f', '', choices_from_function), - ('--function', 'ch', ['choices', 'chatty']), - ('-m', '', choices_from_method), - ('--method', 'm', ['method', 'most']), - ('-i', '', int_choices), - ('--int', '1', ['1 ']), - ('--int', '-', [-1, -2, -12]), - ('--int', '-1', [-1, -12]) -]) + assert ac_app.completion_matches == sorted( + completion_matches, key=ac_app.default_sort_key + ) and ac_app.display_matches == sorted(display_matches, key=ac_app.default_sort_key) + + +@pytest.mark.parametrize( + 'flag, text, completions', + [ + ('-l', '', static_choices_list), + ('--list', 's', ['static', 'stop']), + ('-f', '', choices_from_function), + ('--function', 'ch', ['choices', 'chatty']), + ('-m', '', choices_from_method), + ('--method', 'm', ['method', 'most']), + ('-i', '', int_choices), + ('--int', '1', ['1 ']), + ('--int', '-', [-1, -2, -12]), + ('--int', '-1', [-1, -12]), + ], +) def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): line = 'choices {} {}'.format(flag, text) endidx = len(line) @@ -464,17 +547,20 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): assert ac_app.completion_matches == completions -@pytest.mark.parametrize('pos, text, completions', [ - (1, '', static_choices_list), - (1, 's', ['static', 'stop']), - (2, '', choices_from_function), - (2, 'ch', ['choices', 'chatty']), - (3, '', choices_from_method), - (3, 'm', ['method', 'most']), - (4, '', non_negative_int_choices), - (4, '2', [2, 22]), - (5, '', []), -]) +@pytest.mark.parametrize( + 'pos, text, completions', + [ + (1, '', static_choices_list), + (1, 's', ['static', 'stop']), + (2, '', choices_from_function), + (2, 'ch', ['choices', 'chatty']), + (3, '', choices_from_method), + (3, 'm', ['method', 'most']), + (4, '', non_negative_int_choices), + (4, '2', [2, 22]), + (5, '', []), + ], +) def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): # Generate line were preceding positionals are already filled line = 'choices {} {}'.format('foo ' * (pos - 1), text) @@ -518,12 +604,15 @@ def test_flag_sorting(ac_app): assert first_match is not None and ac_app.completion_matches == option_strings -@pytest.mark.parametrize('flag, text, completions', [ - ('-f', '', completions_from_function), - ('--function', 'f', ['function', 'fairly']), - ('-m', '', completions_from_method), - ('--method', 'm', ['method', 'missed']) -]) +@pytest.mark.parametrize( + 'flag, text, completions', + [ + ('-f', '', completions_from_function), + ('--function', 'f', ['function', 'fairly']), + ('-m', '', completions_from_method), + ('--method', 'm', ['method', 'missed']), + ], +) def test_autocomp_flag_completers(ac_app, flag, text, completions): line = 'completer {} {}'.format(flag, text) endidx = len(line) @@ -538,12 +627,15 @@ def test_autocomp_flag_completers(ac_app, flag, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('pos, text, completions', [ - (1, '', completions_from_function), - (1, 'c', ['completions', 'complete']), - (2, '', completions_from_method), - (2, 'm', ['method', 'missed']) -]) +@pytest.mark.parametrize( + 'pos, text, completions', + [ + (1, '', completions_from_function), + (1, 'c', ['completions', 'complete']), + (2, '', completions_from_method), + (2, 'm', ['method', 'missed']), + ], +) def test_autocomp_positional_completers(ac_app, pos, text, completions): # Generate line were preceding positionals are already filled line = 'completer {} {}'.format('foo ' * (pos - 1), text) @@ -588,14 +680,17 @@ def test_autocomp_blank_token(ac_app): assert completions == completions_from_method -@pytest.mark.parametrize('num_aliases, show_description', [ - # The number of completion results determines if the description field of CompletionItems gets displayed - # in the tab completions. The count must be greater than 1 and less than ac_app.max_completion_items, - # which defaults to 50. - (1, False), - (5, True), - (100, False) -]) +@pytest.mark.parametrize( + 'num_aliases, show_description', + [ + # The number of completion results determines if the description field of CompletionItems gets displayed + # in the tab completions. The count must be greater than 1 and less than ac_app.max_completion_items, + # which defaults to 50. + (1, False), + (5, True), + (100, False), + ], +) def test_completion_items(ac_app, num_aliases, show_description): # Create aliases for i in range(0, num_aliases): @@ -617,71 +712,57 @@ def test_completion_items(ac_app, num_aliases, show_description): assert ('help' in ac_app.display_matches[0]) == show_description -@pytest.mark.parametrize('args, completions', [ - # Flag with nargs = 2 - ('--set_value', set_value_choices), - ('--set_value set', ['value', 'choices']), - - # Both args are filled. At positional arg now. - ('--set_value set value', positional_choices), - - # Using the flag again will reset the choices available - ('--set_value set value --set_value', set_value_choices), - - # Flag with nargs = ONE_OR_MORE - ('--one_or_more', one_or_more_choices), - ('--one_or_more one', ['or', 'more', 'choices']), - - # Flag with nargs = OPTIONAL - ('--optional', optional_choices), - - # Only one arg allowed for an OPTIONAL. At positional now. - ('--optional optional', positional_choices), - - # Flag with nargs range (1, 2) - ('--range', range_choices), - ('--range some', ['range', 'choices']), - - # Already used 2 args so at positional - ('--range some range', positional_choices), - - # Flag with nargs = REMAINDER - ('--remainder', remainder_choices), - ('--remainder remainder ', ['choices ']), - - # No more flags can appear after a REMAINDER flag) - ('--remainder choices --set_value', ['remainder ']), - - # Double dash ends the current flag - ('--range choice --', positional_choices), - - # Double dash ends a REMAINDER flag - ('--remainder remainder --', positional_choices), - - # No more flags after a double dash - ('-- --one_or_more ', positional_choices), - - # Consume positional - ('', positional_choices), - ('positional', ['the', 'choices']), - - # Intermixed flag and positional - ('positional --set_value', set_value_choices), - ('positional --set_value set', ['choices', 'value']), - - # Intermixed flag and positional with flag finishing - ('positional --set_value set value', ['the', 'choices']), - ('positional --range choice --', ['the', 'choices']), - - # REMAINDER positional - ('the positional', remainder_choices), - ('the positional remainder', ['choices ']), - ('the positional remainder choices', []), - - # REMAINDER positional. Flags don't work in REMAINDER - ('the positional --set_value', remainder_choices), - ('the positional remainder --set_value', ['choices ']) -]) +@pytest.mark.parametrize( + 'args, completions', + [ + # Flag with nargs = 2 + ('--set_value', set_value_choices), + ('--set_value set', ['value', 'choices']), + # Both args are filled. At positional arg now. + ('--set_value set value', positional_choices), + # Using the flag again will reset the choices available + ('--set_value set value --set_value', set_value_choices), + # Flag with nargs = ONE_OR_MORE + ('--one_or_more', one_or_more_choices), + ('--one_or_more one', ['or', 'more', 'choices']), + # Flag with nargs = OPTIONAL + ('--optional', optional_choices), + # Only one arg allowed for an OPTIONAL. At positional now. + ('--optional optional', positional_choices), + # Flag with nargs range (1, 2) + ('--range', range_choices), + ('--range some', ['range', 'choices']), + # Already used 2 args so at positional + ('--range some range', positional_choices), + # Flag with nargs = REMAINDER + ('--remainder', remainder_choices), + ('--remainder remainder ', ['choices ']), + # No more flags can appear after a REMAINDER flag) + ('--remainder choices --set_value', ['remainder ']), + # Double dash ends the current flag + ('--range choice --', positional_choices), + # Double dash ends a REMAINDER flag + ('--remainder remainder --', positional_choices), + # No more flags after a double dash + ('-- --one_or_more ', positional_choices), + # Consume positional + ('', positional_choices), + ('positional', ['the', 'choices']), + # Intermixed flag and positional + ('positional --set_value', set_value_choices), + ('positional --set_value set', ['choices', 'value']), + # Intermixed flag and positional with flag finishing + ('positional --set_value set value', ['the', 'choices']), + ('positional --range choice --', ['the', 'choices']), + # REMAINDER positional + ('the positional', remainder_choices), + ('the positional remainder', ['choices ']), + ('the positional remainder choices', []), + # REMAINDER positional. Flags don't work in REMAINDER + ('the positional --set_value', remainder_choices), + ('the positional remainder --set_value', ['choices ']), + ], +) def test_autcomp_nargs(ac_app, args, completions): text = '' line = 'nargs {} {}'.format(args, text) @@ -697,43 +778,39 @@ def test_autcomp_nargs(ac_app, args, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, is_error', [ - # Flag is finished before moving on - ('hint --flag foo --', '', False), - ('hint --flag foo --help', '', False), - ('hint --flag foo', '--', False), - - ('nargs --one_or_more one --', '', False), - ('nargs --one_or_more one or --set_value', '', False), - ('nargs --one_or_more one or more', '--', False), - - ('nargs --set_value set value --', '', False), - ('nargs --set_value set value --one_or_more', '', False), - ('nargs --set_value set value', '--', False), - ('nargs --set_val set value', '--', False), # This exercises our abbreviated flag detection - - ('nargs --range choices --', '', False), - ('nargs --range choices range --set_value', '', False), - ('nargs --range range', '--', False), - - # Flag is not finished before moving on - ('hint --flag --', '', True), - ('hint --flag --help', '', True), - ('hint --flag', '--', True), - - ('nargs --one_or_more --', '', True), - ('nargs --one_or_more --set_value', '', True), - ('nargs --one_or_more', '--', True), - - ('nargs --set_value set --', '', True), - ('nargs --set_value set --one_or_more', '', True), - ('nargs --set_value set', '--', True), - ('nargs --set_val set', '--', True), # This exercises our abbreviated flag detection - - ('nargs --range --', '', True), - ('nargs --range --set_value', '', True), - ('nargs --range', '--', True), -]) +@pytest.mark.parametrize( + 'command_and_args, text, is_error', + [ + # Flag is finished before moving on + ('hint --flag foo --', '', False), + ('hint --flag foo --help', '', False), + ('hint --flag foo', '--', False), + ('nargs --one_or_more one --', '', False), + ('nargs --one_or_more one or --set_value', '', False), + ('nargs --one_or_more one or more', '--', False), + ('nargs --set_value set value --', '', False), + ('nargs --set_value set value --one_or_more', '', False), + ('nargs --set_value set value', '--', False), + ('nargs --set_val set value', '--', False), # This exercises our abbreviated flag detection + ('nargs --range choices --', '', False), + ('nargs --range choices range --set_value', '', False), + ('nargs --range range', '--', False), + # Flag is not finished before moving on + ('hint --flag --', '', True), + ('hint --flag --help', '', True), + ('hint --flag', '--', True), + ('nargs --one_or_more --', '', True), + ('nargs --one_or_more --set_value', '', True), + ('nargs --one_or_more', '--', True), + ('nargs --set_value set --', '', True), + ('nargs --set_value set --one_or_more', '', True), + ('nargs --set_value set', '--', True), + ('nargs --set_val set', '--', True), # This exercises our abbreviated flag detection + ('nargs --range --', '', True), + ('nargs --range --set_value', '', True), + ('nargs --range', '--', True), + ], +) def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -816,35 +893,32 @@ def test_completion_items_descriptive_header(ac_app): assert DEFAULT_DESCRIPTIVE_HEADER in ac_app.completion_header -@pytest.mark.parametrize('command_and_args, text, has_hint', [ - # Normal cases - ('hint', '', True), - ('hint --flag', '', True), - ('hint --suppressed_help', '', False), - ('hint --suppressed_hint', '', False), - - # Hint because flag does not have enough values to be considered finished - ('nargs --one_or_more', '-', True), - - # This flag has reached its minimum value count and therefore a new flag could start. - # However the flag can still consume values and the text is not a single prefix character. - # Therefor a hint will be shown. - ('nargs --one_or_more choices', 'bad_completion', True), - - # Like the previous case, but this time text is a single prefix character which will cause flag - # name completion to occur instead of a hint for the current flag. - ('nargs --one_or_more choices', '-', False), - - # Hint because this is a REMAINDER flag and therefore no more flag name completions occur. - ('nargs --remainder', '-', True), - - # No hint for the positional because text is a single prefix character which results in flag name completion - ('hint', '-', False), - - # Hint because this is a REMAINDER positional and therefore no more flag name completions occur. - ('nargs the choices', '-', True), - ('nargs the choices remainder', '-', True), -]) +@pytest.mark.parametrize( + 'command_and_args, text, has_hint', + [ + # Normal cases + ('hint', '', True), + ('hint --flag', '', True), + ('hint --suppressed_help', '', False), + ('hint --suppressed_hint', '', False), + # Hint because flag does not have enough values to be considered finished + ('nargs --one_or_more', '-', True), + # This flag has reached its minimum value count and therefore a new flag could start. + # However the flag can still consume values and the text is not a single prefix character. + # Therefor a hint will be shown. + ('nargs --one_or_more choices', 'bad_completion', True), + # Like the previous case, but this time text is a single prefix character which will cause flag + # name completion to occur instead of a hint for the current flag. + ('nargs --one_or_more choices', '-', False), + # Hint because this is a REMAINDER flag and therefore no more flag name completions occur. + ('nargs --remainder', '-', True), + # No hint for the positional because text is a single prefix character which results in flag name completion + ('hint', '-', False), + # Hint because this is a REMAINDER positional and therefore no more flag name completions occur. + ('nargs the choices', '-', True), + ('nargs the choices remainder', '-', True), + ], +) def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -868,20 +942,25 @@ def test_autocomp_hint_no_help_text(ac_app, capsys): out, err = capsys.readouterr() assert first_match is None - assert not out == ''' + assert ( + not out + == ''' Hint: NO_HELP_POS ''' - - -@pytest.mark.parametrize('args, text', [ - # Exercise a flag arg and choices function that raises a CompletionError - ('--choice ', 'choice'), - - # Exercise a positional arg and completer that raises a CompletionError - ('', 'completer') -]) + ) + + +@pytest.mark.parametrize( + 'args, text', + [ + # Exercise a flag arg and choices function that raises a CompletionError + ('--choice ', 'choice'), + # Exercise a positional arg and completer that raises a CompletionError + ('', 'completer'), + ], +) def test_completion_error(ac_app, capsys, args, text): line = 'raise_completion_error {} {}'.format(args, text) endidx = len(line) @@ -894,16 +973,17 @@ def test_completion_error(ac_app, capsys, args, text): assert "{} broke something".format(text) in out -@pytest.mark.parametrize('command_and_args, completions', [ - # Exercise a choices function that receives arg_tokens dictionary - ('arg_tokens choice subcmd', ['choice', 'subcmd']), - - # Exercise a completer that receives arg_tokens dictionary - ('arg_tokens completer subcmd fake', ['completer', 'subcmd']), - - # Exercise overriding parent_arg from the subcommand - ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']) -]) +@pytest.mark.parametrize( + 'command_and_args, completions', + [ + # Exercise a choices function that receives arg_tokens dictionary + ('arg_tokens choice subcmd', ['choice', 'subcmd']), + # Exercise a completer that receives arg_tokens dictionary + ('arg_tokens completer subcmd fake', ['completer', 'subcmd']), + # Exercise overriding parent_arg from the subcommand + ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']), + ], +) def test_arg_tokens(ac_app, command_and_args, completions): text = '' line = '{} {}'.format(command_and_args, text) @@ -919,34 +999,29 @@ def test_arg_tokens(ac_app, command_and_args, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -@pytest.mark.parametrize('command_and_args, text, output_contains, first_match', [ - # Group isn't done. Hint will show for optional positional and no completions returned - ('mutex', '', 'the optional positional', None), - - # Group isn't done. Flag name will still complete. - ('mutex', '--fl', '', '--flag '), - - # Group isn't done. Flag hint will show. - ('mutex --flag', '', 'the flag arg', None), - - # Group finished by optional positional. No flag name will complete. - ('mutex pos_val', '--fl', '', None), - - # Group finished by optional positional. Error will display trying to complete the flag's value. - ('mutex pos_val --flag', '', 'f/--flag: not allowed with argument optional_pos', None), - - # Group finished by --flag. Optional positional will be skipped and last_arg will show its hint. - ('mutex --flag flag_val', '', 'the last arg', None), - - # Group finished by --flag. Other flag name won't complete. - ('mutex --flag flag_val', '--oth', '', None), - - # Group finished by --flag. Error will display trying to complete other flag's value. - ('mutex --flag flag_val --other', '', '-o/--other_flag: not allowed with argument -f/--flag', None), - - # Group finished by --flag. That same flag can be used again so it's hint will show. - ('mutex --flag flag_val --flag', '', 'the flag arg', None) -]) +@pytest.mark.parametrize( + 'command_and_args, text, output_contains, first_match', + [ + # Group isn't done. Hint will show for optional positional and no completions returned + ('mutex', '', 'the optional positional', None), + # Group isn't done. Flag name will still complete. + ('mutex', '--fl', '', '--flag '), + # Group isn't done. Flag hint will show. + ('mutex --flag', '', 'the flag arg', None), + # Group finished by optional positional. No flag name will complete. + ('mutex pos_val', '--fl', '', None), + # Group finished by optional positional. Error will display trying to complete the flag's value. + ('mutex pos_val --flag', '', 'f/--flag: not allowed with argument optional_pos', None), + # Group finished by --flag. Optional positional will be skipped and last_arg will show its hint. + ('mutex --flag flag_val', '', 'the last arg', None), + # Group finished by --flag. Other flag name won't complete. + ('mutex --flag flag_val', '--oth', '', None), + # Group finished by --flag. Error will display trying to complete other flag's value. + ('mutex --flag flag_val --other', '', '-o/--other_flag: not allowed with argument -f/--flag', None), + # Group finished by --flag. That same flag can be used again so it's hint will show. + ('mutex --flag flag_val --flag', '', 'the flag arg', None), + ], +) def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, first_match, capsys): line = '{} {}'.format(command_and_args, text) endidx = len(line) @@ -960,6 +1035,7 @@ def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, f def test_single_prefix_char(): from cmd2.argparse_completer import _single_prefix_char + parser = Cmd2ArgumentParser(prefix_chars='-+') # Invalid @@ -976,6 +1052,7 @@ def test_single_prefix_char(): def test_looks_like_flag(): from cmd2.argparse_completer import _looks_like_flag + parser = Cmd2ArgumentParser() # Does not start like a flag diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index e2b3bb97..24a037fa 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -15,6 +15,7 @@ from .conftest import run_cmd class ApCustomTestApp(cmd2.Cmd): """Test app for cmd2's argparse customization""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -40,15 +41,18 @@ def fake_func(): pass -@pytest.mark.parametrize('kwargs, is_valid', [ - ({'choices_function': fake_func}, True), - ({'choices_method': fake_func}, True), - ({'completer_function': fake_func}, True), - ({'completer_method': fake_func}, True), - ({'choices_function': fake_func, 'choices_method': fake_func}, False), - ({'choices_method': fake_func, 'completer_function': fake_func}, False), - ({'completer_function': fake_func, 'completer_method': fake_func}, False), -]) +@pytest.mark.parametrize( + 'kwargs, is_valid', + [ + ({'choices_function': fake_func}, True), + ({'choices_method': fake_func}, True), + ({'completer_function': fake_func}, True), + ({'completer_method': fake_func}, True), + ({'choices_function': fake_func, 'choices_method': fake_func}, False), + ({'choices_method': fake_func, 'completer_function': fake_func}, False), + ({'completer_function': fake_func, 'completer_method': fake_func}, False), + ], +) def test_apcustom_choices_callable_count(kwargs, is_valid): parser = Cmd2ArgumentParser() try: @@ -59,12 +63,15 @@ def test_apcustom_choices_callable_count(kwargs, is_valid): assert 'Only one of the following parameters' in str(ex) -@pytest.mark.parametrize('kwargs', [ - ({'choices_function': fake_func}), - ({'choices_method': fake_func}), - ({'completer_function': fake_func}), - ({'completer_method': fake_func}) -]) +@pytest.mark.parametrize( + 'kwargs', + [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}), + ], +) def test_apcustom_no_choices_callables_alongside_choices(kwargs): with pytest.raises(TypeError) as excinfo: parser = Cmd2ArgumentParser() @@ -72,12 +79,15 @@ def test_apcustom_no_choices_callables_alongside_choices(kwargs): assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value) -@pytest.mark.parametrize('kwargs', [ - ({'choices_function': fake_func}), - ({'choices_method': fake_func}), - ({'completer_function': fake_func}), - ({'completer_method': fake_func}) -]) +@pytest.mark.parametrize( + 'kwargs', + [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}), + ], +) def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs): with pytest.raises(TypeError) as excinfo: parser = Cmd2ArgumentParser() @@ -119,12 +129,7 @@ def test_apcustom_nargs_range_validation(cust_app): assert not err -@pytest.mark.parametrize('nargs_tuple', [ - (), - ('f', 5), - (5, 'f'), - (1, 2, 3), -]) +@pytest.mark.parametrize('nargs_tuple', [(), ('f', 5), (5, 'f'), (1, 2, 3),]) def test_apcustom_narg_invalid_tuples(nargs_tuple): with pytest.raises(ValueError) as excinfo: parser = Cmd2ArgumentParser() @@ -200,6 +205,7 @@ def test_apcustom_narg_tuple_other_ranges(): def test_apcustom_print_message(capsys): import sys + test_message = 'The test message' # Specify the file @@ -247,6 +253,7 @@ def test_apcustom_required_options(): def test_override_parser(): import importlib + from cmd2 import DEFAULT_ARGUMENT_PARSER # The standard parser is Cmd2ArgumentParser @@ -259,6 +266,7 @@ def test_override_parser(): # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser from examples.custom_parser import CustomParser + assert DEFAULT_ARGUMENT_PARSER == CustomParser diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 2f24f4d7..ae911474 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -40,15 +40,19 @@ def CreateOutsimApp(): c.stdout = utils.StdSim(c.stdout) return c + @pytest.fixture def outsim_app(): return CreateOutsimApp() + def test_version(base_app): assert cmd2.__version__ + def test_not_in_main_thread(base_app, capsys): import threading + cli_thread = threading.Thread(name='cli_thread', target=base_app.cmdloop) cli_thread.start() @@ -56,15 +60,18 @@ def test_not_in_main_thread(base_app, capsys): out, err = capsys.readouterr() assert "cmdloop must be run in the main thread" in err + def test_empty_statement(base_app): out, err = run_cmd(base_app, '') expected = normalize('') assert out == expected + def test_base_help(base_app): out, err = run_cmd(base_app, 'help') verify_help_text(base_app, out) + def test_base_help_verbose(base_app): out, err = run_cmd(base_app, 'help -v') verify_help_text(base_app, out) @@ -78,6 +85,7 @@ def test_base_help_verbose(base_app): verify_help_text(base_app, out) assert ':param' not in ''.join(out) + def test_base_argparse_help(base_app): # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense out1, err1 = run_cmd(base_app, 'set -h') @@ -88,21 +96,25 @@ def test_base_argparse_help(base_app): assert out1[1] == '' assert out1[2].startswith('Set a settable parameter') + def test_base_invalid_option(base_app): out, err = run_cmd(base_app, 'set -z') assert err[0] == 'Usage: set [-h] [-v] [param] [value]' assert 'Error: unrecognized arguments: -z' in err[1] + def test_base_shortcuts(base_app): out, err = run_cmd(base_app, 'shortcuts') expected = normalize(SHORTCUTS_TXT) assert out == expected + def test_command_starts_with_shortcut(): with pytest.raises(ValueError) as excinfo: app = cmd2.Cmd(shortcuts={'help': 'fake'}) assert "Invalid command name 'help'" in str(excinfo.value) + def test_base_show(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' @@ -121,30 +133,37 @@ def test_base_show_long(base_app): def test_set(base_app): out, err = run_cmd(base_app, 'set quiet True') - expected = normalize(""" + expected = normalize( + """ quiet - was: False now: True -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] + def test_set_val_empty(base_app): base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor ""') assert base_app.editor == '' + def test_set_val_is_flag(base_app): base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor "-h"') assert base_app.editor == '-h' + def test_set_not_supported(base_app): out, err = run_cmd(base_app, 'set qqq True') - expected = normalize(""" + expected = normalize( + """ Parameter 'qqq' not supported (type 'set' for list of parameters). -""") +""" + ) assert err == expected @@ -155,15 +174,18 @@ def test_set_no_settables(base_app): assert err == expected -@pytest.mark.parametrize('new_val, is_valid, expected', [ - (ansi.STYLE_NEVER, True, ansi.STYLE_NEVER), - ('neVeR', True, ansi.STYLE_NEVER), - (ansi.STYLE_TERMINAL, True, ansi.STYLE_TERMINAL), - ('TeRMInal', True, ansi.STYLE_TERMINAL), - (ansi.STYLE_ALWAYS, True, ansi.STYLE_ALWAYS), - ('AlWaYs', True, ansi.STYLE_ALWAYS), - ('invalid', False, ansi.STYLE_TERMINAL), -]) +@pytest.mark.parametrize( + 'new_val, is_valid, expected', + [ + (ansi.STYLE_NEVER, True, ansi.STYLE_NEVER), + ('neVeR', True, ansi.STYLE_NEVER), + (ansi.STYLE_TERMINAL, True, ansi.STYLE_TERMINAL), + ('TeRMInal', True, ansi.STYLE_TERMINAL), + (ansi.STYLE_ALWAYS, True, ansi.STYLE_ALWAYS), + ('AlWaYs', True, ansi.STYLE_ALWAYS), + ('invalid', False, ansi.STYLE_TERMINAL), + ], +) def test_set_allow_style(base_app, new_val, is_valid, expected): # Initialize allow_style for this test ansi.allow_style = ansi.STYLE_TERMINAL @@ -190,18 +212,22 @@ class OnChangeHookApp(cmd2.Cmd): """Runs when quiet is changed via set command""" self.poutput("You changed " + name) + @pytest.fixture def onchange_app(): app = OnChangeHookApp() return app + def test_set_onchange_hook(onchange_app): out, err = run_cmd(onchange_app, 'set quiet True') - expected = normalize(""" + expected = normalize( + """ quiet - was: False now: True You changed quiet -""") +""" + ) assert out == expected @@ -212,6 +238,7 @@ def test_base_shell(base_app, monkeypatch): assert out == [] assert m.called + def test_shell_last_result(base_app): base_app.last_result = None run_cmd(base_app, 'shell fake') @@ -220,11 +247,7 @@ def test_shell_last_result(base_app): def test_shell_manual_call(base_app): # Verifies crash from Issue #986 doesn't happen - cmds = [ - 'echo "hi"', - 'echo "there"', - 'echo "cmd2!"' - ] + cmds = ['echo "hi"', 'echo "there"', 'echo "cmd2!"'] cmd = ';'.join(cmds) base_app.do_shell(cmd) @@ -299,31 +322,37 @@ def test_run_script(base_app, request): assert script_out == manual_out assert script_err == manual_err + def test_run_script_with_empty_args(base_app): out, err = run_cmd(base_app, 'run_script') assert "the following arguments are required" in err[1] + def test_run_script_with_nonexistent_file(base_app, capsys): out, err = run_cmd(base_app, 'run_script does_not_exist.txt') assert "does not exist" in err[0] + def test_run_script_with_directory(base_app, request): test_dir = os.path.dirname(request.module.__file__) out, err = run_cmd(base_app, 'run_script {}'.format(test_dir)) assert "is not a file" in err[0] + def test_run_script_with_empty_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'empty.txt') out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert not out and not err + def test_run_script_with_binary_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'binary.bin') out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert "is not an ASCII or UTF-8 encoded text file" in err[0] + def test_run_script_with_python_file(base_app, request): m = mock.MagicMock(name='input', return_value='2') builtins.input = m @@ -333,6 +362,7 @@ def test_run_script_with_python_file(base_app, request): out, err = run_cmd(base_app, 'run_script {}'.format(filename)) assert "appears to be a Python file" in err[0] + def test_run_script_with_utf8_file(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') @@ -360,6 +390,7 @@ def test_run_script_with_utf8_file(base_app, request): assert script_out == manual_out assert script_err == manual_err + def test_run_script_nested_run_scripts(base_app, request): # Verify that running a script with nested run_script commands works correctly, # and runs the nested script commands in the correct order. @@ -371,43 +402,48 @@ def test_run_script_nested_run_scripts(base_app, request): run_cmd(base_app, initial_run) # Check that the right commands were executed. - expected = """ + expected = ( + """ %s _relative_run_script precmds.txt set allow_style Always help shortcuts _relative_run_script postcmds.txt -set allow_style Never""" % initial_run +set allow_style Never""" + % initial_run + ) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) + def test_runcmds_plus_hooks(base_app, request): test_dir = os.path.dirname(request.module.__file__) prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt') postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt') - base_app.runcmds_plus_hooks(['run_script ' + prefilepath, - 'help', - 'shortcuts', - 'run_script ' + postfilepath]) + base_app.runcmds_plus_hooks(['run_script ' + prefilepath, 'help', 'shortcuts', 'run_script ' + postfilepath]) expected = """ -run_script %s +run_script {} set allow_style Always help shortcuts -run_script %s -set allow_style Never""" % (prefilepath, postfilepath) +run_script {} +set allow_style Never""".format( + prefilepath, postfilepath, + ) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) + def test_runcmds_plus_hooks_ctrl_c(base_app, capsys): """Test Ctrl-C while in runcmds_plus_hooks""" import types def do_keyboard_interrupt(self, _): raise KeyboardInterrupt('Interrupting this command') + setattr(base_app, 'do_keyboard_interrupt', types.MethodType(do_keyboard_interrupt, base_app)) # Default behavior is to stop command loop on Ctrl-C @@ -424,6 +460,7 @@ def test_runcmds_plus_hooks_ctrl_c(base_app, capsys): assert not err assert len(base_app.history) == 3 + def test_relative_run_script(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') @@ -451,6 +488,7 @@ def test_relative_run_script(base_app, request): assert script_out == manual_out assert script_err == manual_err + @pytest.mark.parametrize('file_name', odd_file_names) def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch): """Test file names with various patterns""" @@ -461,10 +499,12 @@ def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatc run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name))) run_script_mock.assert_called_once_with(utils.quote_string(file_name)) + def test_relative_run_script_requires_an_argument(base_app): out, err = run_cmd(base_app, '_relative_run_script') assert 'Error: the following arguments' in err[1] + def test_in_script(request): class HookApp(cmd2.Cmd): def __init__(self, *args, **kwargs): @@ -483,17 +523,20 @@ def test_in_script(request): assert "WE ARE IN SCRIPT" in out[-1] + def test_system_exit_in_command(base_app, capsys): """Test raising SystemExit from a command""" import types def do_system_exit(self, _): raise SystemExit + setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app)) stop = base_app.onecmd_plus_hooks('system_exit') assert stop + def test_output_redirection(base_app): fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(fd) @@ -516,6 +559,7 @@ def test_output_redirection(base_app): finally: os.remove(filename) + def test_output_redirection_to_nonexistent_directory(base_app): filename = '~/fakedir/this_does_not_exist.txt' @@ -525,12 +569,15 @@ def test_output_redirection_to_nonexistent_directory(base_app): out, err = run_cmd(base_app, 'help >> {}'.format(filename)) assert 'Failed to redirect' in err[0] + def test_output_redirection_to_too_long_filename(base_app): - filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' \ - 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' \ - 'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu' \ - 'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie' \ - 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + filename = ( + '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' + 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' + 'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu' + 'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie' + 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' + ) out, err = run_cmd(base_app, 'help > {}'.format(filename)) assert 'Failed to redirect' in err[0] @@ -588,6 +635,7 @@ def test_disallow_redirection(base_app): # Verify that no file got created assert not os.path.exists(filename) + def test_pipe_to_shell(base_app): if sys.platform == "win32": # Windows @@ -600,6 +648,7 @@ def test_pipe_to_shell(base_app): out, err = run_cmd(base_app, command) assert out and not err + def test_pipe_to_shell_and_redirect(base_app): filename = 'out.txt' if sys.platform == "win32": @@ -615,14 +664,15 @@ def test_pipe_to_shell_and_redirect(base_app): assert os.path.exists(filename) os.remove(filename) + def test_pipe_to_shell_error(base_app): # Try to pipe command output to a shell command that doesn't exist in order to produce an error out, err = run_cmd(base_app, 'help | foobarbaz.this_does_not_exist') assert not out assert "Pipe process exited with code" in err[0] -@pytest.mark.skipif(not clipboard.can_clip, - reason="Pyperclip could not find a copy/paste mechanism for your system") + +@pytest.mark.skipif(not clipboard.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system") def test_send_to_paste_buffer(base_app): # Test writing to the PasteBuffer/Clipboard run_cmd(base_app, 'help >') @@ -639,9 +689,11 @@ def test_send_to_paste_buffer(base_app): def test_base_timing(base_app): base_app.feedback_to_output = False out, err = run_cmd(base_app, 'set timing True') - expected = normalize("""timing - was: False + expected = normalize( + """timing - was: False now: True -""") +""" + ) assert out == expected if sys.platform == 'win32': @@ -656,13 +708,18 @@ def _expected_no_editor_error(): if hasattr(sys, "pypy_translation_info"): expected_exception = 'EnvironmentError' - expected_text = normalize(""" + expected_text = normalize( + """ EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.' To enable full traceback, run the following command: 'set debug true' -""".format(expected_exception)) +""".format( + expected_exception + ) + ) return expected_text + def test_base_debug(base_app): # Purposely set the editor to None base_app.editor = None @@ -675,16 +732,19 @@ def test_base_debug(base_app): # Set debug true out, err = run_cmd(base_app, 'set debug True') - expected = normalize(""" + expected = normalize( + """ debug - was: False now: True -""") +""" + ) assert out == expected # Verify that we now see the exception traceback out, err = run_cmd(base_app, 'edit') assert err[0].startswith('Traceback (most recent call last):') + def test_debug_not_settable(base_app): # Set debug to False and make it unsettable base_app.debug = False @@ -696,10 +756,12 @@ def test_debug_not_settable(base_app): # Since debug is unsettable, the user will not be given the option to enable a full traceback assert err == ['Invalid syntax: No closing quotation'] + def test_remove_settable_keyerror(base_app): with pytest.raises(KeyError): base_app.remove_settable('fake') + def test_edit_file(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -716,6 +778,7 @@ def test_edit_file(base_app, request, monkeypatch): # We think we have an editor, so should expect a Popen call m.assert_called_once() + @pytest.mark.parametrize('file_name', odd_file_names) def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): """Test editor and file names with various patterns""" @@ -728,6 +791,7 @@ def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): run_cmd(base_app, "edit {}".format(utils.quote_string(file_name))) shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name))) + def test_edit_file_with_spaces(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -744,6 +808,7 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch): # We think we have an editor, so should expect a Popen call m.assert_called_once() + def test_edit_blank(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' @@ -830,8 +895,8 @@ def test_cmdloop_without_rawinput(): out = app.stdout.getvalue() assert out == expected -@pytest.mark.skipif(sys.platform.startswith('win'), - reason="stty sane only run on Linux/Mac") + +@pytest.mark.skipif(sys.platform.startswith('win'), reason="stty sane only run on Linux/Mac") def test_stty_sane(base_app, monkeypatch): """Make sure stty sane is run on Linux/Mac after each command if stdin is a terminal""" with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)): @@ -842,6 +907,7 @@ def test_stty_sane(base_app, monkeypatch): base_app.onecmd_plus_hooks('help') m.assert_called_once_with(['stty', 'sane']) + class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -853,11 +919,13 @@ class HookFailureApp(cmd2.Cmd): data.stop = True return data + @pytest.fixture def hook_failure(): app = HookFailureApp() return app + def test_precmd_hook_success(base_app): out = base_app.onecmd_plus_hooks('help') assert out is False @@ -875,12 +943,14 @@ class SayApp(cmd2.Cmd): def do_say(self, arg): self.poutput(arg) + @pytest.fixture def say_app(): app = SayApp(allow_cli_args=False) app.stdout = utils.StdSim(app.stdout) return app + def test_interrupt_quit(say_app): say_app.quit_on_sigint = True @@ -898,6 +968,7 @@ def test_interrupt_quit(say_app): out = say_app.stdout.getvalue() assert out == 'hello\n' + def test_interrupt_noquit(say_app): say_app.quit_on_sigint = False @@ -921,6 +992,7 @@ class ShellApp(cmd2.Cmd): super().__init__(*args, **kwargs) self.default_to_shell = True + def test_default_to_shell(base_app, monkeypatch): if sys.platform.startswith('win'): line = 'dir' @@ -934,14 +1006,17 @@ def test_default_to_shell(base_app, monkeypatch): assert out == [] assert m.called + def test_ansi_prompt_not_esacped(base_app): from cmd2.rl_utils import rl_make_safe_prompt + prompt = '(Cmd) ' assert rl_make_safe_prompt(prompt) == prompt def test_ansi_prompt_escaped(): from cmd2.rl_utils import rl_make_safe_prompt + app = cmd2.Cmd() color = 'cyan' prompt = 'InColor' @@ -963,6 +1038,7 @@ def test_ansi_prompt_escaped(): class HelpApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -996,24 +1072,29 @@ def help_app(): app = HelpApp() return app + def test_custom_command_help(help_app): out, err = run_cmd(help_app, 'help squat') expected = normalize('This command does diddly squat...') assert out == expected + def test_custom_help_menu(help_app): out, err = run_cmd(help_app, 'help') verify_help_text(help_app, out) + def test_help_undocumented(help_app): out, err = run_cmd(help_app, 'help undoc') assert err[0].startswith("No help on undoc") + def test_help_overridden_method(help_app): out, err = run_cmd(help_app, 'help edit') expected = normalize('This overrides the edit command and does nothing.') assert out == expected + def test_help_multiline_docstring(help_app): out, err = run_cmd(help_app, 'help multiline_docstr') expected = normalize('This documentation\nis multiple lines\nand there are no\ntabs') @@ -1022,6 +1103,7 @@ def test_help_multiline_docstring(help_app): class HelpCategoriesApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1052,15 +1134,18 @@ class HelpCategoriesApp(cmd2.Cmd): def do_undoc(self, arg): pass + @pytest.fixture def helpcat_app(): app = HelpCategoriesApp() return app + def test_help_cat_base(helpcat_app): out, err = run_cmd(helpcat_app, 'help') verify_help_text(helpcat_app, out) + def test_help_cat_verbose(helpcat_app): out, err = run_cmd(helpcat_app, 'help --verbose') verify_help_text(helpcat_app, out) @@ -1085,8 +1170,9 @@ class SelectApp(cmd2.Cmd): def do_procrastinate(self, arg): """Waste time in your manner of choice.""" # Pass in a list of tuples for selections - leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')], - 'How would you like to procrastinate? ') + leisure_activity = self.select( + [('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')], 'How would you like to procrastinate? ' + ) result = 'Have fun procrasinating with {}!\n'.format(leisure_activity) self.stdout.write(result) @@ -1097,11 +1183,13 @@ class SelectApp(cmd2.Cmd): result = 'Charm us with the {}...\n'.format(instrument) self.stdout.write(result) + @pytest.fixture def select_app(): app = SelectApp() return app + def test_select_options(select_app, monkeypatch): # Mock out the read_input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') @@ -1109,11 +1197,15 @@ def test_select_options(select_app, monkeypatch): food = 'bacon' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty {} with salty sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Sauce? ') @@ -1121,6 +1213,7 @@ def test_select_options(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_invalid_option_too_big(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input') @@ -1131,12 +1224,16 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): food = 'fish' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty '3' isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' @@ -1147,6 +1244,7 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_invalid_option_too_small(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input') @@ -1157,12 +1255,16 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): food = 'fish' out, err = run_cmd(select_app, "eat {}".format(food)) - expected = normalize(""" + expected = normalize( + """ 1. sweet 2. salty '0' isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! -""".format(food)) +""".format( + food + ) + ) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' @@ -1173,17 +1275,22 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): # And verify the expected output to stdout assert out == expected + def test_select_list_of_strings(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "study") - expected = normalize(""" + expected = normalize( + """ 1. math 2. science Good luck learning {}! -""".format('science')) +""".format( + 'science' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Subject? ') @@ -1191,17 +1298,22 @@ Good luck learning {}! # And verify the expected output to stdout assert out == expected + def test_select_list_of_tuples(select_app, monkeypatch): # Mock out the input call so we don't actually wait for a user's response on stdin read_input_mock = mock.MagicMock(name='read_input', return_value='2') monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "procrastinate") - expected = normalize(""" + expected = normalize( + """ 1. Netflix 2. WebSurfing Have fun procrasinating with {}! -""".format('YouTube')) +""".format( + 'YouTube' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('How would you like to procrastinate? ') @@ -1216,11 +1328,15 @@ def test_select_uneven_list_of_tuples(select_app, monkeypatch): monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "play") - expected = normalize(""" + expected = normalize( + """ 1. Electric Guitar 2. Drums Charm us with the {}... -""".format('Drums')) +""".format( + 'Drums' + ) + ) # Make sure our mock was called with the expected arguments read_input_mock.assert_called_once_with('Instrument? ') @@ -1228,6 +1344,7 @@ Charm us with the {}... # And verify the expected output to stdout assert out == expected + def test_select_eof(select_app, monkeypatch): # Ctrl-D during select causes an EOFError that just reprompts the user read_input_mock = mock.MagicMock(name='read_input', side_effect=[EOFError, 2]) @@ -1242,6 +1359,7 @@ def test_select_eof(select_app, monkeypatch): read_input_mock.assert_has_calls(calls) assert read_input_mock.call_count == 2 + def test_select_ctrl_c(outsim_app, monkeypatch, capsys): # Ctrl-C during select prints ^C and raises a KeyboardInterrupt read_input_mock = mock.MagicMock(name='read_input', side_effect=KeyboardInterrupt) @@ -1253,9 +1371,11 @@ def test_select_ctrl_c(outsim_app, monkeypatch, capsys): out = outsim_app.stdout.getvalue() assert out.rstrip().endswith('^C') + class HelpNoDocstringApp(cmd2.Cmd): greet_parser = argparse.ArgumentParser() greet_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") + @cmd2.with_argparser(greet_parser, with_unknown_args=True) def do_greet(self, opts, arg): arg = ''.join(arg) @@ -1263,17 +1383,22 @@ class HelpNoDocstringApp(cmd2.Cmd): arg = arg.upper() self.stdout.write(arg + '\n') + def test_help_with_no_docstring(capsys): app = HelpNoDocstringApp() app.onecmd_plus_hooks('greet -h') out, err = capsys.readouterr() assert err == '' - assert out == """usage: greet [-h] [-s] + assert ( + out + == """usage: greet [-h] [-s] optional arguments: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE """ + ) + class MultilineApp(cmd2.Cmd): def __init__(self, *args, **kwargs): @@ -1289,15 +1414,18 @@ class MultilineApp(cmd2.Cmd): arg = arg.upper() self.stdout.write(arg + '\n') + @pytest.fixture def multiline_app(): app = MultilineApp() return app + def test_multiline_complete_empty_statement_raises_exception(multiline_app): with pytest.raises(exceptions.EmptyStatement): multiline_app._complete_statement('') + def test_multiline_complete_statement_without_terminator(multiline_app): # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input @@ -1312,6 +1440,7 @@ def test_multiline_complete_statement_without_terminator(multiline_app): assert statement.command == command assert statement.multiline_command == command + def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input @@ -1325,6 +1454,7 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): assert statement.multiline_command == 'orate' assert statement.terminator == ';' + def test_multiline_input_line_to_statement(multiline_app): # Verify _input_line_to_statement saves the fully entered input line for multiline commands @@ -1340,6 +1470,7 @@ def test_multiline_input_line_to_statement(multiline_app): assert statement.command == 'orate' assert statement.multiline_command == 'orate' + def test_clipboard_failure(base_app, capsys): # Force cmd2 clipboard to be disabled base_app._can_clip = False @@ -1369,11 +1500,13 @@ class CommandResultApp(cmd2.Cmd): def do_negative_no_data(self, arg): self.last_result = cmd2.CommandResult('', arg) + @pytest.fixture def commandresult_app(): app = CommandResultApp() return app + def test_commandresult_truthy(commandresult_app): arg = 'foo' run_cmd(commandresult_app, 'affirmative {}'.format(arg)) @@ -1384,6 +1517,7 @@ def test_commandresult_truthy(commandresult_app): assert commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult(arg) + def test_commandresult_falsy(commandresult_app): arg = 'bar' run_cmd(commandresult_app, 'negative {}'.format(arg)) @@ -1409,6 +1543,7 @@ def test_eof(base_app): # Only thing to verify is that it returns True assert base_app.do_eof('') + def test_echo(capsys): app = cmd2.Cmd() app.echo = True @@ -1419,6 +1554,7 @@ def test_echo(capsys): out, err = capsys.readouterr() assert out.startswith('{}{}\n'.format(app.prompt, commands[0]) + HELP_HISTORY.split()[0]) + def test_read_input_rawinput_true(capsys, monkeypatch): prompt_str = 'the_prompt' input_str = 'some input' @@ -1450,6 +1586,7 @@ def test_read_input_rawinput_true(capsys, monkeypatch): assert line == input_str assert not out + def test_read_input_rawinput_false(capsys, monkeypatch): prompt_str = 'the_prompt' input_str = 'some input' @@ -1502,6 +1639,7 @@ def test_read_input_rawinput_false(capsys, monkeypatch): assert line == 'eof' assert not out + def test_read_command_line_eof(base_app, monkeypatch): read_input_mock = mock.MagicMock(name='read_input', side_effect=EOFError) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) @@ -1509,6 +1647,7 @@ def test_read_command_line_eof(base_app, monkeypatch): line = base_app._read_command_line("Prompt> ") assert line == 'eof' + def test_poutput_string(outsim_app): msg = 'This is a test' outsim_app.poutput(msg) @@ -1516,6 +1655,7 @@ def test_poutput_string(outsim_app): expected = msg + '\n' assert out == expected + def test_poutput_zero(outsim_app): msg = 0 outsim_app.poutput(msg) @@ -1523,6 +1663,7 @@ def test_poutput_zero(outsim_app): expected = str(msg) + '\n' assert out == expected + def test_poutput_empty_string(outsim_app): msg = '' outsim_app.poutput(msg) @@ -1530,6 +1671,7 @@ def test_poutput_empty_string(outsim_app): expected = '\n' assert out == expected + def test_poutput_none(outsim_app): msg = None outsim_app.poutput(msg) @@ -1537,6 +1679,7 @@ def test_poutput_none(outsim_app): expected = 'None\n' assert out == expected + def test_poutput_ansi_always(outsim_app): msg = 'Hello World' ansi.allow_style = ansi.STYLE_ALWAYS @@ -1547,6 +1690,7 @@ def test_poutput_ansi_always(outsim_app): assert colored_msg != msg assert out == expected + def test_poutput_ansi_never(outsim_app): msg = 'Hello World' ansi.allow_style = ansi.STYLE_NEVER @@ -1571,6 +1715,7 @@ invalid_command_name = [ 'noembedded"quotes', ] + def test_get_alias_completion_items(base_app): run_cmd(base_app, 'alias create fake run_pyscript') run_cmd(base_app, 'alias create ls !ls -hal') @@ -1582,6 +1727,7 @@ def test_get_alias_completion_items(base_app): assert cur_res in base_app.aliases assert cur_res.description == base_app.aliases[cur_res] + def test_get_macro_completion_items(base_app): run_cmd(base_app, 'macro create foo !echo foo') run_cmd(base_app, 'macro create bar !echo bar') @@ -1593,17 +1739,20 @@ def test_get_macro_completion_items(base_app): assert cur_res in base_app.macros assert cur_res.description == base_app.macros[cur_res].value + def test_get_settable_completion_items(base_app): results = base_app._get_settable_completion_items() for cur_res in results: assert cur_res in base_app.settables assert cur_res.description == base_app.settables[cur_res].description + def test_alias_no_subcommand(base_app): out, err = run_cmd(base_app, 'alias') assert "Usage: alias [-h]" in err[0] assert "Error: the following arguments are required: SUBCOMMAND" in err[1] + def test_alias_create(base_app): # Create the alias out, err = run_cmd(base_app, 'alias create fake run_pyscript') @@ -1636,6 +1785,7 @@ def test_alias_create(base_app): out, err = run_cmd(base_app, 'alias list --with_silent fake') assert out == normalize('alias create --silent fake set') + def test_alias_create_with_quoted_tokens(base_app): """Demonstrate that quotes in alias value will be preserved""" create_command = 'alias create fake help ">" "out file.txt" ";"' @@ -1648,21 +1798,25 @@ def test_alias_create_with_quoted_tokens(base_app): out, err = run_cmd(base_app, 'alias list fake') assert out == normalize(create_command) + @pytest.mark.parametrize('alias_name', invalid_command_name) def test_alias_create_invalid_name(base_app, alias_name, capsys): out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name)) assert "Invalid alias name" in err[0] + def test_alias_create_with_command_name(base_app): out, err = run_cmd(base_app, 'alias create help stuff') assert "Alias cannot have the same name as a command" in err[0] + def test_alias_create_with_macro_name(base_app): macro = "my_macro" run_cmd(base_app, 'macro create {} help'.format(macro)) out, err = run_cmd(base_app, 'alias create {} help'.format(macro)) assert "Alias cannot have the same name as a macro" in err[0] + def test_alias_that_resolves_into_comment(base_app): # Create the alias out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah') @@ -1673,11 +1827,13 @@ def test_alias_that_resolves_into_comment(base_app): assert not out assert not err + def test_alias_list_invalid_alias(base_app): # Look up invalid alias out, err = run_cmd(base_app, 'alias list invalid') assert "Alias 'invalid' not found" in err[0] + def test_alias_delete(base_app): # Create an alias run_cmd(base_app, 'alias create fake run_pyscript') @@ -1686,18 +1842,22 @@ def test_alias_delete(base_app): out, err = run_cmd(base_app, 'alias delete fake') assert out == normalize("Alias 'fake' deleted") + def test_alias_delete_all(base_app): out, err = run_cmd(base_app, 'alias delete --all') assert out == normalize("All aliases deleted") + def test_alias_delete_non_existing(base_app): out, err = run_cmd(base_app, 'alias delete fake') assert "Alias 'fake' does not exist" in err[0] + def test_alias_delete_no_name(base_app): out, err = run_cmd(base_app, 'alias delete') assert "Either --all or alias name(s)" in err[0] + def test_multiple_aliases(base_app): alias1 = 'h1' alias2 = 'h2' @@ -1709,11 +1869,13 @@ def test_multiple_aliases(base_app): out, err = run_cmd(base_app, alias2) verify_help_text(base_app, out) + def test_macro_no_subcommand(base_app): out, err = run_cmd(base_app, 'macro') assert "Usage: macro [-h]" in err[0] assert "Error: the following arguments are required: SUBCOMMAND" in err[1] + def test_macro_create(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript') @@ -1746,6 +1908,7 @@ def test_macro_create(base_app): out, err = run_cmd(base_app, 'macro list --with_silent fake') assert out == normalize('macro create --silent fake set') + def test_macro_create_with_quoted_tokens(base_app): """Demonstrate that quotes in macro value will be preserved""" create_command = 'macro create fake help ">" "out file.txt" ";"' @@ -1758,21 +1921,25 @@ def test_macro_create_with_quoted_tokens(base_app): out, err = run_cmd(base_app, 'macro list fake') assert out == normalize(create_command) + @pytest.mark.parametrize('macro_name', invalid_command_name) def test_macro_create_invalid_name(base_app, macro_name): out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name)) assert "Invalid macro name" in err[0] + def test_macro_create_with_command_name(base_app): out, err = run_cmd(base_app, 'macro create help stuff') assert "Macro cannot have the same name as a command" in err[0] + def test_macro_create_with_alias_name(base_app): macro = "my_macro" run_cmd(base_app, 'alias create {} help'.format(macro)) out, err = run_cmd(base_app, 'macro create {} help'.format(macro)) assert "Macro cannot have the same name as an alias" in err[0] + def test_macro_create_with_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} {2}') @@ -1782,6 +1949,7 @@ def test_macro_create_with_args(base_app): out, err = run_cmd(base_app, 'fake help -v') verify_help_text(base_app, out) + def test_macro_create_with_escaped_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {{1}}') @@ -1791,6 +1959,7 @@ def test_macro_create_with_escaped_args(base_app): out, err = run_cmd(base_app, 'fake') assert err[0].startswith('No help on {1}') + def test_macro_usage_with_missing_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {2}') @@ -1800,6 +1969,7 @@ def test_macro_usage_with_missing_args(base_app): out, err = run_cmd(base_app, 'fake arg1') assert "expects at least 2 argument(s)" in err[0] + def test_macro_usage_with_exta_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1}') @@ -1809,16 +1979,19 @@ def test_macro_usage_with_exta_args(base_app): out, err = run_cmd(base_app, 'fake alias create') assert "Usage: alias create" in out[0] + def test_macro_create_with_missing_arg_nums(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {3}') assert "Not all numbers between 1 and 3" in err[0] + def test_macro_create_with_invalid_arg_num(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}') assert "Argument numbers must be greater than 0" in err[0] + def test_macro_create_with_unicode_numbered_arg(base_app): # Create the macro expecting 1 argument out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}') @@ -1828,10 +2001,12 @@ def test_macro_create_with_unicode_numbered_arg(base_app): out, err = run_cmd(base_app, 'fake') assert "expects at least 1 argument(s)" in err[0] + def test_macro_create_with_missing_unicode_arg_nums(base_app): out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}') assert "Not all numbers between 1 and 3" in err[0] + def test_macro_that_resolves_into_comment(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} blah blah') @@ -1842,11 +2017,13 @@ def test_macro_that_resolves_into_comment(base_app): assert not out assert not err + def test_macro_list_invalid_macro(base_app): # Look up invalid macro out, err = run_cmd(base_app, 'macro list invalid') assert "Macro 'invalid' not found" in err[0] + def test_macro_delete(base_app): # Create an macro run_cmd(base_app, 'macro create fake run_pyscript') @@ -1855,18 +2032,22 @@ def test_macro_delete(base_app): out, err = run_cmd(base_app, 'macro delete fake') assert out == normalize("Macro 'fake' deleted") + def test_macro_delete_all(base_app): out, err = run_cmd(base_app, 'macro delete --all') assert out == normalize("All macros deleted") + def test_macro_delete_non_existing(base_app): out, err = run_cmd(base_app, 'macro delete fake') assert "Macro 'fake' does not exist" in err[0] + def test_macro_delete_no_name(base_app): out, err = run_cmd(base_app, 'macro delete') assert "Either --all or macro name(s)" in err[0] + def test_multiple_macros(base_app): macro1 = 'h1' macro2 = 'h2' @@ -1879,8 +2060,10 @@ def test_multiple_macros(base_app): verify_help_text(base_app, out2) assert len(out2) > len(out) + def test_nonexistent_macro(base_app): from cmd2.parsing import StatementParser + exception = None try: @@ -1890,6 +2073,7 @@ def test_nonexistent_macro(base_app): assert exception is not None + def test_perror_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1898,6 +2082,7 @@ def test_perror_style(base_app, capsys): out, err = capsys.readouterr() assert err == ansi.style_error(msg) + end + def test_perror_no_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1906,6 +2091,7 @@ def test_perror_no_style(base_app, capsys): out, err = capsys.readouterr() assert err == msg + end + def test_pwarning_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1914,6 +2100,7 @@ def test_pwarning_style(base_app, capsys): out, err = capsys.readouterr() assert err == ansi.style_warning(msg) + end + def test_pwarning_no_style(base_app, capsys): msg = 'testing...' end = '\n' @@ -1922,6 +2109,7 @@ def test_pwarning_no_style(base_app, capsys): out, err = capsys.readouterr() assert err == msg + end + def test_ppaged(outsim_app): msg = 'testing...' end = '\n' @@ -1929,18 +2117,21 @@ def test_ppaged(outsim_app): out = outsim_app.stdout.getvalue() assert out == msg + end + def test_ppaged_blank(outsim_app): msg = '' outsim_app.ppaged(msg) out = outsim_app.stdout.getvalue() assert not out + def test_ppaged_none(outsim_app): msg = None outsim_app.ppaged(msg) out = outsim_app.stdout.getvalue() assert not out + def test_ppaged_strips_ansi_when_redirecting(outsim_app): msg = 'testing...' end = '\n' @@ -1950,6 +2141,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app): out = outsim_app.stdout.getvalue() assert out == msg + end + def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): msg = 'testing...' end = '\n' @@ -1960,6 +2152,7 @@ def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): out = outsim_app.stdout.getvalue() assert out == colored_msg + end + # we override cmd.parseline() so we always get consistent # command parsing by parent methods we don't override # don't need to test all the parsing logic here, because @@ -1971,6 +2164,7 @@ def test_parseline_empty(base_app): assert not args assert not line + def test_parseline(base_app): statement = " command with 'partially completed quotes " command, args, line = base_app.parseline(statement) @@ -1986,6 +2180,7 @@ def test_onecmd_raw_str_continue(outsim_app): assert not stop verify_help_text(outsim_app, out) + def test_onecmd_raw_str_quit(outsim_app): line = "quit" stop = outsim_app.onecmd(line) @@ -1993,6 +2188,7 @@ def test_onecmd_raw_str_quit(outsim_app): assert stop assert out == '' + def test_onecmd_add_to_history(outsim_app): line = "help" saved_hist_len = len(outsim_app.history) @@ -2009,18 +2205,35 @@ def test_onecmd_add_to_history(outsim_app): new_hist_len = len(outsim_app.history) assert new_hist_len == saved_hist_len + def test_get_all_commands(base_app): # Verify that the base app has the expected commands commands = base_app.get_all_commands() - expected_commands = ['_relative_run_script', 'alias', 'edit', 'eof', 'help', 'history', 'macro', - 'py', 'quit', 'run_pyscript', 'run_script', 'set', 'shell', 'shortcuts'] + expected_commands = [ + '_relative_run_script', + 'alias', + 'edit', + 'eof', + 'help', + 'history', + 'macro', + 'py', + 'quit', + 'run_pyscript', + 'run_script', + 'set', + 'shell', + 'shortcuts', + ] assert commands == expected_commands + def test_get_help_topics(base_app): # Verify that the base app has no additional help_foo methods custom_help = base_app.get_help_topics() assert len(custom_help) == 0 + def test_get_help_topics_hidden(): # Verify get_help_topics() filters out hidden commands class TestApp(cmd2.Cmd): @@ -2039,6 +2252,7 @@ def test_get_help_topics_hidden(): app.hidden_commands.append('my_cmd') assert 'my_cmd' not in app.get_help_topics() + class ReplWithExitCode(cmd2.Cmd): """ Example cmd2 application where we can specify an exit code when existing.""" @@ -2068,12 +2282,14 @@ Usage: exit [exit_code] """Hook method executed once when the cmdloop() method is about to return.""" self.poutput('exiting with code: {}'.format(self.exit_code)) + @pytest.fixture def exit_code_repl(): app = ReplWithExitCode() app.stdout = utils.StdSim(app.stdout) return app + def test_exit_code_default(exit_code_repl): app = exit_code_repl app.use_rawinput = True @@ -2089,6 +2305,7 @@ def test_exit_code_default(exit_code_repl): out = app.stdout.getvalue() assert out == expected + def test_exit_code_nonzero(exit_code_repl): app = exit_code_repl app.use_rawinput = True @@ -2118,6 +2335,7 @@ class AnsiApp(cmd2.Cmd): # perror uses colors by default self.perror(args) + def test_ansi_pouterr_always_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_ALWAYS @@ -2140,6 +2358,7 @@ def test_ansi_pouterr_always_tty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_pouterr_always_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_ALWAYS @@ -2162,6 +2381,7 @@ def test_ansi_pouterr_always_notty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_terminal_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_TERMINAL @@ -2183,6 +2403,7 @@ def test_ansi_terminal_tty(mocker, capsys): assert len(err) > len('oopsie\n') assert 'oopsie' in err + def test_ansi_terminal_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_TERMINAL @@ -2197,6 +2418,7 @@ def test_ansi_terminal_notty(mocker, capsys): out, err = capsys.readouterr() assert out == err == 'oopsie\n' + def test_ansi_never_tty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_NEVER @@ -2211,6 +2433,7 @@ def test_ansi_never_tty(mocker, capsys): out, err = capsys.readouterr() assert out == err == 'oopsie\n' + def test_ansi_never_notty(mocker, capsys): app = AnsiApp() ansi.allow_style = ansi.STYLE_NEVER @@ -2228,6 +2451,7 @@ def test_ansi_never_notty(mocker, capsys): class DisableCommandsApp(cmd2.Cmd): """Class for disabling commands""" + category_name = "Test Category" def __init__(self, *args, **kwargs): @@ -2346,6 +2570,7 @@ def test_disable_and_enable_category(disable_commands_app): help_topics = disable_commands_app.get_help_topics() assert 'has_helper_funcs' in help_topics + def test_enable_enabled_command(disable_commands_app): # Test enabling a command that is not disabled saved_len = len(disable_commands_app.disabled_commands) @@ -2354,10 +2579,12 @@ def test_enable_enabled_command(disable_commands_app): # The number of disabled_commands should not have changed assert saved_len == len(disable_commands_app.disabled_commands) + def test_disable_fake_command(disable_commands_app): with pytest.raises(AttributeError): disable_commands_app.disable_command('fake', 'fake message') + def test_disable_command_twice(disable_commands_app): saved_len = len(disable_commands_app.disabled_commands) message_to_print = 'These commands are currently disabled' @@ -2373,6 +2600,7 @@ def test_disable_command_twice(disable_commands_app): new_len = len(disable_commands_app.disabled_commands) assert saved_len == new_len + def test_disabled_command_not_in_history(disable_commands_app): message_to_print = 'These commands are currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) @@ -2381,6 +2609,7 @@ def test_disabled_command_not_in_history(disable_commands_app): run_cmd(disable_commands_app, 'has_helper_funcs') assert saved_len == len(disable_commands_app.history) + def test_disabled_message_command_name(disable_commands_app): message_to_print = '{} is currently disabled'.format(COMMAND_NAME) disable_commands_app.disable_command('has_helper_funcs', message_to_print) diff --git a/tests/test_completion.py b/tests/test_completion.py index db243f48..785bb49d 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -28,44 +28,42 @@ from .conftest import complete_tester, normalize, run_cmd # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato', 'Cheese "Pizza"'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] -delimited_strs = \ - [ - '/home/user/file.txt', - '/home/user/file space.txt', - '/home/user/prog.c', - '/home/other user/maps', - '/home/other user/tests' - ] +delimited_strs = [ + '/home/user/file.txt', + '/home/user/file space.txt', + '/home/user/prog.c', + '/home/other user/maps', + '/home/other user/tests', +] # Dictionary used with flag based completion functions -flag_dict = \ - { - # Tab complete food items after -f and --food flag in command line - '-f': food_item_strs, - '--food': food_item_strs, - - # Tab complete sport items after -s and --sport flag in command line - '-s': sport_item_strs, - '--sport': sport_item_strs, - } +flag_dict = { + # Tab complete food items after -f and --food flag in command line + '-f': food_item_strs, + '--food': food_item_strs, + # Tab complete sport items after -s and --sport flag in command line + '-s': sport_item_strs, + '--sport': sport_item_strs, +} # Dictionary used with index based completion functions -index_dict = \ - { - 1: food_item_strs, # Tab complete food items at index 1 in command line - 2: sport_item_strs, # Tab complete sport items at index 2 in command line - } +index_dict = { + 1: food_item_strs, # Tab complete food items at index 1 in command line + 2: sport_item_strs, # Tab complete sport items at index 2 in command line +} class CompletionsExample(cmd2.Cmd): """ Example cmd2 application used to exercise tab completion tests """ + def __init__(self): cmd2.Cmd.__init__(self, multiline_commands=['test_multiline']) self.foo = 'bar' - self.add_settable(utils.Settable('foo', str, description="a settable param", - completer_method=CompletionsExample.complete_foo_val)) + self.add_settable( + utils.Settable('foo', str, description="a settable param", completer_method=CompletionsExample.complete_foo_val) + ) def do_test_basic(self, args): pass @@ -130,6 +128,7 @@ def test_cmd2_command_completion_single(cmd2_app): begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] + def test_complete_command_single(cmd2_app): text = 'he' line = text @@ -139,6 +138,7 @@ def test_complete_command_single(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['help '] + def test_complete_empty_arg(cmd2_app): text = '' line = 'help {}'.format(text) @@ -150,6 +150,7 @@ def test_complete_empty_arg(cmd2_app): assert first_match is not None and cmd2_app.completion_matches == expected + def test_complete_bogus_command(cmd2_app): text = '' line = 'fizbuzz {}'.format(text) @@ -160,6 +161,7 @@ def test_complete_bogus_command(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_complete_exception(cmd2_app, capsys): text = '' line = 'test_raise_exception {}'.format(text) @@ -172,6 +174,7 @@ def test_complete_exception(cmd2_app, capsys): assert first_match is None assert "IndexError" in err + def test_complete_macro(base_app, request): # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}') @@ -217,6 +220,7 @@ def test_cmd2_command_completion_multiple(cmd2_app): begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] + def test_cmd2_command_completion_nomatch(cmd2_app): text = 'fakecommand' line = text @@ -236,6 +240,7 @@ def test_cmd2_help_completion_single(cmd2_app): # It is at end of line, so extra space is present assert first_match is not None and cmd2_app.completion_matches == ['help '] + def test_cmd2_help_completion_multiple(cmd2_app): text = 'h' line = 'help {}'.format(text) @@ -274,9 +279,8 @@ def test_shell_command_completion_shortcut(cmd2_app): begidx = 0 first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - cmd2_app.completion_matches == expected and \ - cmd2_app.display_matches == expected_display + assert first_match is not None and cmd2_app.completion_matches == expected and cmd2_app.display_matches == expected_display + def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): if sys.platform == "win32": @@ -291,6 +295,7 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_multiple(cmd2_app): if sys.platform == "win32": text = 'c' @@ -306,6 +311,7 @@ def test_shell_command_completion_multiple(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and expected in cmd2_app.completion_matches + def test_shell_command_completion_nomatch(cmd2_app): text = 'zzzz' line = 'shell {}'.format(text) @@ -315,6 +321,7 @@ def test_shell_command_completion_nomatch(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): text = '' line = 'shell {}'.format(text) @@ -324,6 +331,7 @@ def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None + def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -336,6 +344,7 @@ def test_shell_command_completion_does_path_completion_when_after_command(cmd2_a first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == [text + '.py '] + def test_shell_commmand_complete_in_path(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -363,6 +372,7 @@ def test_path_completion_single_end(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx) == [text + '.py'] + def test_path_completion_multiple(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -376,6 +386,7 @@ def test_path_completion_multiple(cmd2_app, request): expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] assert matches == expected + def test_path_completion_nomatch(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -431,6 +442,7 @@ def test_path_completion_no_text(cmd2_app): assert completions_no_text == completions_cwd assert completions_cwd + def test_path_completion_no_path(cmd2_app): # Run path complete with search text that isn't preceded by a path. This should use CWD as the path. text = 's' @@ -471,6 +483,7 @@ def test_path_completion_cwd_is_root_dir(cmd2_app): # Restore CWD os.chdir(cwd) + def test_path_completion_doesnt_match_wildcards(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -483,10 +496,14 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request): # Currently path completion doesn't accept wildcards, so will always return empty results assert cmd2_app.path_complete(text, line, begidx, endidx) == [] -@pytest.mark.skipif(sys.platform == 'win32', reason="getpass.getuser() does not work on Windows in AppVeyor because " - "no user name environment variables are set") + +@pytest.mark.skipif( + sys.platform == 'win32', + reason="getpass.getuser() does not work on Windows in AppVeyor because " "no user name environment variables are set", +) def test_path_completion_complete_user(cmd2_app): import getpass + user = getpass.getuser() text = '~{}'.format(user) @@ -498,6 +515,7 @@ def test_path_completion_complete_user(cmd2_app): expected = text + os.path.sep assert expected in completions + def test_path_completion_user_path_expansion(cmd2_app): # Run path with a tilde and a slash if sys.platform.startswith('win'): @@ -510,8 +528,7 @@ def test_path_completion_user_path_expansion(cmd2_app): line = 'shell {} {}'.format(cmd, text) endidx = len(line) begidx = endidx - len(text) - completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, - begidx, endidx)] + completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] # Run path complete on the user's home directory text = os.path.expanduser('~') + os.path.sep @@ -522,6 +539,7 @@ def test_path_completion_user_path_expansion(cmd2_app): assert completions_tilde_slash == completions_home + def test_path_completion_directories_only(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -535,6 +553,7 @@ def test_path_completion_directories_only(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) == expected + def test_basic_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) @@ -543,6 +562,7 @@ def test_basic_completion_single(cmd2_app): assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] + def test_basic_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) @@ -552,6 +572,7 @@ def test_basic_completion_multiple(cmd2_app): matches = sorted(utils.basic_complete(text, line, begidx, endidx, food_item_strs)) assert matches == sorted(food_item_strs) + def test_basic_completion_nomatch(cmd2_app): text = 'q' line = 'list_food -f {}'.format(text) @@ -560,6 +581,7 @@ def test_basic_completion_nomatch(cmd2_app): assert utils.basic_complete(text, line, begidx, endidx, food_item_strs) == [] + def test_delimiter_completion(cmd2_app): text = '/home/' line = 'run_script {}'.format(text) @@ -574,6 +596,7 @@ def test_delimiter_completion(cmd2_app): assert display_list == ['other user', 'user'] + def test_flag_based_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) @@ -582,6 +605,7 @@ def test_flag_based_completion_single(cmd2_app): assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza'] + def test_flag_based_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) @@ -591,6 +615,7 @@ def test_flag_based_completion_multiple(cmd2_app): matches = sorted(cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict)) assert matches == sorted(food_item_strs) + def test_flag_based_completion_nomatch(cmd2_app): text = 'q' line = 'list_food -f {}'.format(text) @@ -599,6 +624,7 @@ def test_flag_based_completion_nomatch(cmd2_app): assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [] + def test_flag_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -608,8 +634,10 @@ def test_flag_based_default_completer(cmd2_app, request): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.flag_based_complete(text, line, begidx, endidx, - flag_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] + assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=cmd2_app.path_complete) == [ + text + 'onftest.py' + ] + def test_flag_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -621,8 +649,7 @@ def test_flag_based_callable_completer(cmd2_app, request): begidx = endidx - len(text) flag_dict['-o'] = cmd2_app.path_complete - assert cmd2_app.flag_based_complete(text, line, begidx, endidx, - flag_dict) == [text + 'onftest.py'] + assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [text + 'onftest.py'] def test_index_based_completion_single(cmd2_app): @@ -633,6 +660,7 @@ def test_index_based_completion_single(cmd2_app): assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == ['Football'] + def test_index_based_completion_multiple(cmd2_app): text = '' line = 'command Pizza {}'.format(text) @@ -642,6 +670,7 @@ def test_index_based_completion_multiple(cmd2_app): matches = sorted(cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict)) assert matches == sorted(sport_item_strs) + def test_index_based_completion_nomatch(cmd2_app): text = 'q' line = 'command {}'.format(text) @@ -649,6 +678,7 @@ def test_index_based_completion_nomatch(cmd2_app): begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [] + def test_index_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -658,8 +688,10 @@ def test_index_based_default_completer(cmd2_app, request): endidx = len(line) begidx = endidx - len(text) - assert cmd2_app.index_based_complete(text, line, begidx, endidx, - index_dict, all_else=cmd2_app.path_complete) == [text + 'onftest.py'] + assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict, all_else=cmd2_app.path_complete) == [ + text + 'onftest.py' + ] + def test_index_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) @@ -687,6 +719,7 @@ def test_tokens_for_completion_quoted(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_unclosed_quote(cmd2_app): text = 'Pi' line = 'list_food "{}'.format(text) @@ -700,6 +733,7 @@ def test_tokens_for_completion_unclosed_quote(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_punctuation(cmd2_app): """Test that redirectors and terminators are word delimiters""" text = 'file' @@ -714,6 +748,7 @@ def test_tokens_for_completion_punctuation(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_tokens_for_completion_quoted_punctuation(cmd2_app): """Test that quoted punctuation characters are not word delimiters""" text = '>file' @@ -728,6 +763,7 @@ def test_tokens_for_completion_quoted_punctuation(cmd2_app): assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens + def test_add_opening_quote_basic_no_text(cmd2_app): text = '' line = 'test_basic {}'.format(text) @@ -736,8 +772,8 @@ def test_add_opening_quote_basic_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, - key=cmd2_app.default_sort_key) + assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, key=cmd2_app.default_sort_key) + def test_add_opening_quote_basic_nothing_added(cmd2_app): text = 'P' @@ -748,6 +784,7 @@ def test_add_opening_quote_basic_nothing_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['Pizza', 'Potato'] + def test_add_opening_quote_basic_quote_added(cmd2_app): text = 'Ha' line = 'test_basic {}'.format(text) @@ -758,6 +795,7 @@ def test_add_opening_quote_basic_quote_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_basic_single_quote_added(cmd2_app): text = 'Ch' line = 'test_basic {}'.format(text) @@ -768,6 +806,7 @@ def test_add_opening_quote_basic_single_quote_added(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches text = 'Ham' @@ -779,6 +818,7 @@ def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_add_opening_quote_delimited_no_text(cmd2_app): text = '' line = 'test_delimited {}'.format(text) @@ -787,8 +827,8 @@ def test_add_opening_quote_delimited_no_text(cmd2_app): # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, - key=cmd2_app.default_sort_key) + assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key) + def test_add_opening_quote_delimited_nothing_added(cmd2_app): text = '/ho' @@ -800,9 +840,12 @@ def test_add_opening_quote_delimited_nothing_added(cmd2_app): expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - cmd2_app.completion_matches == expected_matches and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and cmd2_app.completion_matches == expected_matches + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_quote_added(cmd2_app): text = '/home/user/fi' @@ -814,9 +857,12 @@ def test_add_opening_quote_delimited_quote_added(cmd2_app): expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches @@ -829,9 +875,12 @@ def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): # This test when a space appears before the part of the string that is the display match @@ -844,9 +893,12 @@ def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): expected_display = ['maps', 'tests'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and \ - os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ - cmd2_app.display_matches == expected_display + assert ( + first_match is not None + and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + and cmd2_app.display_matches == expected_display + ) + def test_no_completer(cmd2_app): text = '' @@ -858,6 +910,7 @@ def test_no_completer(cmd2_app): first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected + def test_quote_as_command(cmd2_app): text = '' line = '" {}'.format(text) @@ -898,36 +951,39 @@ def test_complete_multiline_on_multiple_lines(cmd2_app): # Used by redirect_complete tests class RedirCompType(enum.Enum): - SHELL_CMD = 1, - PATH = 2, - DEFAULT = 3, + SHELL_CMD = (1,) + PATH = (2,) + DEFAULT = (3,) NONE = 4 -@pytest.mark.parametrize('line, comp_type', [ - ('fake', RedirCompType.DEFAULT), - ('fake arg', RedirCompType.DEFAULT), - ('fake |', RedirCompType.SHELL_CMD), - ('fake | grep', RedirCompType.PATH), - ('fake | grep arg', RedirCompType.PATH), - ('fake | grep >', RedirCompType.PATH), - ('fake | grep > >', RedirCompType.NONE), - ('fake | grep > file', RedirCompType.NONE), - ('fake | grep > file >', RedirCompType.NONE), - ('fake | grep > file |', RedirCompType.SHELL_CMD), - ('fake | grep > file | grep', RedirCompType.PATH), - ('fake | |', RedirCompType.NONE), - ('fake | >', RedirCompType.NONE), - ('fake >', RedirCompType.PATH), - ('fake >>', RedirCompType.PATH), - ('fake > >', RedirCompType.NONE), - ('fake > |', RedirCompType.SHELL_CMD), - ('fake >> file |', RedirCompType.SHELL_CMD), - ('fake >> file | grep', RedirCompType.PATH), - ('fake > file', RedirCompType.NONE), - ('fake > file >', RedirCompType.NONE), - ('fake > file >>', RedirCompType.NONE), -]) +@pytest.mark.parametrize( + 'line, comp_type', + [ + ('fake', RedirCompType.DEFAULT), + ('fake arg', RedirCompType.DEFAULT), + ('fake |', RedirCompType.SHELL_CMD), + ('fake | grep', RedirCompType.PATH), + ('fake | grep arg', RedirCompType.PATH), + ('fake | grep >', RedirCompType.PATH), + ('fake | grep > >', RedirCompType.NONE), + ('fake | grep > file', RedirCompType.NONE), + ('fake | grep > file >', RedirCompType.NONE), + ('fake | grep > file |', RedirCompType.SHELL_CMD), + ('fake | grep > file | grep', RedirCompType.PATH), + ('fake | |', RedirCompType.NONE), + ('fake | >', RedirCompType.NONE), + ('fake >', RedirCompType.PATH), + ('fake >>', RedirCompType.PATH), + ('fake > >', RedirCompType.NONE), + ('fake > |', RedirCompType.SHELL_CMD), + ('fake >> file |', RedirCompType.SHELL_CMD), + ('fake >> file | grep', RedirCompType.PATH), + ('fake > file', RedirCompType.NONE), + ('fake > file >', RedirCompType.NONE), + ('fake > file >>', RedirCompType.NONE), + ], +) def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): # Test both cases of allow_redirection cmd2_app.allow_redirection = True @@ -974,6 +1030,7 @@ def test_complete_set_value(cmd2_app): assert first_match == "SUCCESS " assert cmd2_app.completion_hint == "Hint:\n value a settable param\n" + def test_complete_set_value_invalid_settable(cmd2_app, capsys): text = '' line = 'set fake {}'.format(text) @@ -986,12 +1043,14 @@ def test_complete_set_value_invalid_settable(cmd2_app, capsys): out, err = capsys.readouterr() assert "fake is not a settable parameter" in out + @pytest.fixture def sc_app(): c = SubcommandsExample() c.stdout = utils.StdSim(c.stdout) return c + def test_cmd2_subcommand_completion_single_end(sc_app): text = 'f' line = 'base {}'.format(text) @@ -1003,6 +1062,7 @@ def test_cmd2_subcommand_completion_single_end(sc_app): # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['foo '] + def test_cmd2_subcommand_completion_multiple(sc_app): text = '' line = 'base {}'.format(text) @@ -1012,6 +1072,7 @@ def test_cmd2_subcommand_completion_multiple(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport'] + def test_cmd2_subcommand_completion_nomatch(sc_app): text = 'z' line = 'base {}'.format(text) @@ -1033,6 +1094,7 @@ def test_help_subcommand_completion_single(sc_app): # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['base '] + def test_help_subcommand_completion_multiple(sc_app): text = '' line = 'help base {}'.format(text) @@ -1052,6 +1114,7 @@ def test_help_subcommand_completion_nomatch(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is None + def test_subcommand_tab_completion(sc_app): # This makes sure the correct completer for the sport subcommand is called text = 'Foot' @@ -1085,9 +1148,8 @@ def test_subcommand_tab_completion_space_in_text(sc_app): first_match = complete_tester(text, line, begidx, endidx, sc_app) - assert first_match is not None and \ - sc_app.completion_matches == ['Ball" '] and \ - sc_app.display_matches == ['Space Ball'] + assert first_match is not None and sc_app.completion_matches == ['Ball" '] and sc_app.display_matches == ['Space Ball'] + #################################################### @@ -1207,6 +1269,7 @@ def test_help_subcommand_completion_multiple_scu(scu_app): first_match = complete_tester(text, line, begidx, endidx, scu_app) assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + def test_help_subcommand_completion_with_flags_before_command(scu_app): text = '' line = 'help -h -v base {}'.format(text) @@ -1216,6 +1279,7 @@ def test_help_subcommand_completion_with_flags_before_command(scu_app): first_match = complete_tester(text, line, begidx, endidx, scu_app) assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + def test_complete_help_subcommands_with_blank_command(scu_app): text = '' line = 'help "" {}'.format(text) @@ -1269,6 +1333,4 @@ def test_subcommand_tab_completion_space_in_text_scu(scu_app): first_match = complete_tester(text, line, begidx, endidx, scu_app) - assert first_match is not None and \ - scu_app.completion_matches == ['Ball" '] and \ - scu_app.display_matches == ['Space Ball'] + assert first_match is not None and scu_app.completion_matches == ['Ball" '] and scu_app.display_matches == ['Space Ball'] diff --git a/tests/test_history.py b/tests/test_history.py index 6fa16ad8..cba6f3ce 100755 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -9,6 +9,7 @@ import tempfile import pytest import cmd2 + # Python 3.5 had some regressions in the unitest.mock module, so use # 3rd party mock if available from cmd2.parsing import StatementParser @@ -21,44 +22,57 @@ except ImportError: from unittest import mock - # # readline tests # def test_readline_remove_history_item(base_app): from cmd2.rl_utils import readline + assert readline.get_current_history_length() == 0 readline.add_history('this is a test') assert readline.get_current_history_length() == 1 readline.remove_history_item(0) assert readline.get_current_history_length() == 0 + # # test History() class # @pytest.fixture def hist(): - from cmd2.parsing import Statement from cmd2.cmd2 import History, HistoryItem - h = History([HistoryItem(Statement('', raw='first'), 1), - HistoryItem(Statement('', raw='second'), 2), - HistoryItem(Statement('', raw='third'), 3), - HistoryItem(Statement('', raw='fourth'),4)]) + from cmd2.parsing import Statement + + h = History( + [ + HistoryItem(Statement('', raw='first'), 1), + HistoryItem(Statement('', raw='second'), 2), + HistoryItem(Statement('', raw='third'), 3), + HistoryItem(Statement('', raw='fourth'), 4), + ] + ) return h + @pytest.fixture def persisted_hist(): - from cmd2.parsing import Statement from cmd2.cmd2 import History, HistoryItem - h = History([HistoryItem(Statement('', raw='first'), 1), - HistoryItem(Statement('', raw='second'), 2), - HistoryItem(Statement('', raw='third'), 3), - HistoryItem(Statement('', raw='fourth'),4)]) + from cmd2.parsing import Statement + + h = History( + [ + HistoryItem(Statement('', raw='first'), 1), + HistoryItem(Statement('', raw='second'), 2), + HistoryItem(Statement('', raw='third'), 3), + HistoryItem(Statement('', raw='fourth'), 4), + ] + ) h.start_session() h.append(Statement('', raw='fifth')) h.append(Statement('', raw='sixth')) return h + def test_history_class_span(hist): for tryit in ['*', ':', '-', 'all', 'ALL']: assert hist.span(tryit) == hist @@ -135,6 +149,7 @@ def test_history_class_span(hist): with pytest.raises(ValueError): hist.span(tryit) + def test_persisted_history_span(persisted_hist): for tryit in ['*', ':', '-', 'all', 'ALL']: assert persisted_hist.span(tryit, include_persisted=True) == persisted_hist @@ -191,6 +206,7 @@ def test_persisted_history_span(persisted_hist): with pytest.raises(ValueError): persisted_hist.span(tryit) + def test_history_class_get(hist): assert hist.get('1').statement.raw == 'first' assert hist.get(3).statement.raw == 'third' @@ -217,6 +233,7 @@ def test_history_class_get(hist): with pytest.raises(TypeError): hist.get(None) + def test_history_str_search(hist): items = hist.str_search('ir') assert len(items) == 2 @@ -227,6 +244,7 @@ def test_history_str_search(hist): assert len(items) == 1 assert items[0].statement.raw == 'fourth' + def test_history_regex_search(hist): items = hist.regex_search('/i.*d/') assert len(items) == 1 @@ -236,52 +254,59 @@ def test_history_regex_search(hist): assert len(items) == 1 assert items[0].statement.raw == 'second' + def test_history_max_length_zero(hist): hist.truncate(0) assert len(hist) == 0 + def test_history_max_length_negative(hist): hist.truncate(-1) assert len(hist) == 0 + def test_history_max_length(hist): hist.truncate(2) assert len(hist) == 2 assert hist.get(1).statement.raw == 'third' assert hist.get(2).statement.raw == 'fourth' + # # test HistoryItem() # @pytest.fixture def histitem(): - from cmd2.parsing import Statement from cmd2.history import HistoryItem - statement = Statement('history', - raw='help history', - command='help', - arg_list=['history'], - ) + from cmd2.parsing import Statement + + statement = Statement('history', raw='help history', command='help', arg_list=['history'],) histitem = HistoryItem(statement, 1) return histitem + @pytest.fixture def parser(): from cmd2.parsing import StatementParser + parser = StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], - aliases={'helpalias': 'help', - '42': 'theanswer', - 'l': '!ls -al', - 'anothermultiline': 'multiline', - 'fake': 'run_pyscript'}, - shortcuts={'?': 'help', '!': 'shell'} + aliases={ + 'helpalias': 'help', + '42': 'theanswer', + 'l': '!ls -al', + 'anothermultiline': 'multiline', + 'fake': 'run_pyscript', + }, + shortcuts={'?': 'help', '!': 'shell'}, ) return parser + def test_multiline_histitem(parser): from cmd2.history import History + line = 'multiline foo\nbar\n\n' statement = parser.parse(line) history = History() @@ -292,8 +317,10 @@ def test_multiline_histitem(parser): pr_lines = hist_item.pr().splitlines() assert pr_lines[0].endswith('multiline foo bar') + def test_multiline_histitem_verbose(parser): from cmd2.history import History + line = 'multiline foo\nbar\n\n' statement = parser.parse(line) history = History() @@ -305,14 +332,12 @@ def test_multiline_histitem_verbose(parser): assert pr_lines[0].endswith('multiline foo') assert pr_lines[1] == 'bar' + def test_history_item_instantiate(): - from cmd2.parsing import Statement from cmd2.history import HistoryItem - statement = Statement('history', - raw='help history', - command='help', - arg_list=['history'], - ) + from cmd2.parsing import Statement + + statement = Statement('history', raw='help history', command='help', arg_list=['history'],) with pytest.raises(TypeError): _ = HistoryItem() with pytest.raises(TypeError): @@ -322,11 +347,13 @@ def test_history_item_instantiate(): with pytest.raises(TypeError): _ = HistoryItem(statement=statement, idx='hi') + def test_history_item_properties(histitem): assert histitem.raw == 'help history' assert histitem.expanded == 'help history' assert str(histitem) == 'help history' + # # test history command # @@ -334,113 +361,144 @@ def test_base_history(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'history he') - expected = normalize(""" + expected = normalize( + """ 1 help -""") +""" + ) assert out == expected out, err = run_cmd(base_app, 'history sh') - expected = normalize(""" + expected = normalize( + """ 2 shortcuts -""") +""" + ) assert out == expected + def test_history_script_format(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history -s') - expected = normalize(""" + expected = normalize( + """ help shortcuts -""") +""" + ) assert out == expected + def test_history_with_string_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history help') - expected = normalize(""" + expected = normalize( + """ 1 help 3 help history -""") +""" + ) assert out == expected + def test_history_expanded_with_string_argument(base_app): run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, 'sc') out, err = run_cmd(base_app, 'history -v shortcuts') - expected = normalize(""" + expected = normalize( + """ 1 alias create sc shortcuts 4 sc 4x shortcuts -""") +""" + ) assert out == expected + def test_history_expanded_with_regex_argument(base_app): run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, 'sc') out, err = run_cmd(base_app, 'history -v /sh.*cuts/') - expected = normalize(""" + expected = normalize( + """ 1 alias create sc shortcuts 4 sc 4x shortcuts -""") +""" + ) assert out == expected + def test_history_with_integer_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history 1') - expected = normalize(""" + expected = normalize( + """ 1 help -""") +""" + ) assert out == expected + def test_history_with_integer_span(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history 1..2') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected + def test_history_with_span_start(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history 2:') - expected = normalize(""" + expected = normalize( + """ 2 shortcuts 3 help history -""") +""" + ) assert out == expected + def test_history_with_span_end(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out, err = run_cmd(base_app, 'history :2') - expected = normalize(""" + expected = normalize( + """ 1 help 2 shortcuts -""") +""" + ) assert out == expected + def test_history_with_span_index_error(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'help history') @@ -448,6 +506,7 @@ def test_history_with_span_index_error(base_app): with pytest.raises(ValueError): base_app.onecmd('history "hal :"') + def test_history_output_file(): app = cmd2.Cmd(multiline_commands=['alias']) run_cmd(app, 'help') @@ -463,6 +522,7 @@ def test_history_output_file(): content = normalize(f.read()) assert content == expected + def test_history_bad_output_file(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') @@ -474,6 +534,7 @@ def test_history_bad_output_file(base_app): assert not out assert "Error saving" in err[0] + def test_history_edit(monkeypatch): app = cmd2.Cmd(multiline_commands=['alias']) @@ -499,6 +560,7 @@ def test_history_edit(monkeypatch): edit_mock.assert_called_once() run_script_mock.assert_called_once() + def test_history_run_all_commands(base_app): # make sure we refuse to run all commands as a default run_cmd(base_app, 'shortcuts') @@ -509,11 +571,13 @@ def test_history_run_all_commands(base_app): # then we should have a list of shortcuts in our output assert out == [] + def test_history_run_one_command(base_app): out1, err1 = run_cmd(base_app, 'help') out2, err2 = run_cmd(base_app, 'history -r 1') assert out1 == out2 + def test_history_clear(hist_file): # Add commands to history app = cmd2.Cmd(persistent_history_file=hist_file) @@ -532,6 +596,7 @@ def test_history_clear(hist_file): assert out == [] assert not os.path.exists(hist_file) + def test_history_verbose_with_other_options(base_app): # make sure -v shows a usage error if any other options are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x'] @@ -541,6 +606,7 @@ def test_history_verbose_with_other_options(base_app): assert out[0] == '-v can not be used with any other options' assert out[1].startswith('Usage:') + def test_history_verbose(base_app): # validate function of -v option run_cmd(base_app, 'alias create s shortcuts') @@ -549,6 +615,7 @@ def test_history_verbose(base_app): assert len(out) == 3 # TODO test for basic formatting once we figure it out + def test_history_script_with_invalid_options(base_app): # make sure -s shows a usage error if -c, -r, -e, -o, or -t are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] @@ -558,6 +625,7 @@ def test_history_script_with_invalid_options(base_app): assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') + def test_history_script(base_app): cmds = ['alias create s shortcuts', 's'] for cmd in cmds: @@ -565,6 +633,7 @@ def test_history_script(base_app): out, err = run_cmd(base_app, 'history -s') assert out == cmds + def test_history_expanded_with_invalid_options(base_app): # make sure -x shows a usage error if -c, -r, -e, -o, or -t are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c'] @@ -574,6 +643,7 @@ def test_history_expanded_with_invalid_options(base_app): assert out[0] == '-s and -x can not be used with -c, -r, -e, -o, or -t' assert out[1].startswith('Usage:') + def test_history_expanded(base_app): # validate function of -x option cmds = ['alias create s shortcuts', 's'] @@ -583,6 +653,7 @@ def test_history_expanded(base_app): expected = [' 1 alias create s shortcuts', ' 2 shortcuts'] assert out == expected + def test_history_script_expanded(base_app): # validate function of -s -x options together cmds = ['alias create s shortcuts', 's'] @@ -592,10 +663,12 @@ def test_history_script_expanded(base_app): expected = ['alias create s shortcuts', 'shortcuts'] assert out == expected + def test_base_help_history(base_app): out, err = run_cmd(base_app, 'help history') assert out == normalize(HELP_HISTORY) + def test_exclude_from_history(base_app, monkeypatch): # Run history command run_cmd(base_app, 'history') @@ -612,6 +685,7 @@ def test_exclude_from_history(base_app, monkeypatch): expected = normalize(""" 1 help""") assert out == expected + # # test history initialization # @@ -626,6 +700,7 @@ def hist_file(): except FileNotFoundError: pass + def test_history_file_is_directory(capsys): with tempfile.TemporaryDirectory() as test_dir: # Create a new cmd2 app @@ -633,6 +708,7 @@ def test_history_file_is_directory(capsys): _, err = capsys.readouterr() assert 'is a directory' in err + def test_history_can_create_directory(mocker): # Mock out atexit.register so the persistent file doesn't written when this function # exists because we will be deleting the directory it needs to go to. @@ -654,6 +730,7 @@ def test_history_can_create_directory(mocker): # Cleanup os.rmdir(hist_file_dir) + def test_history_cannot_create_directory(mocker, capsys): mock_open = mocker.patch('os.makedirs') mock_open.side_effect = OSError @@ -663,6 +740,7 @@ def test_history_cannot_create_directory(mocker, capsys): _, err = capsys.readouterr() assert 'Error creating persistent history file directory' in err + def test_history_file_permission_error(mocker, capsys): mock_open = mocker.patch('builtins.open') mock_open.side_effect = PermissionError @@ -672,6 +750,7 @@ def test_history_file_permission_error(mocker, capsys): assert not out assert 'Can not read' in err + def test_history_file_conversion_no_truncate_on_init(hist_file, capsys): # make sure we don't truncate the plain text history file on init # it shouldn't get converted to pickle format until we save history @@ -688,14 +767,15 @@ def test_history_file_conversion_no_truncate_on_init(hist_file, capsys): # history should be initialized, but the file on disk should # still be plain text with open(hist_file, 'r') as hfobj: - histlist= hfobj.readlines() + histlist = hfobj.readlines() assert len(histlist) == 3 # history.get() is overridden to be one based, not zero based - assert histlist[0]== 'help\n' + assert histlist[0] == 'help\n' assert histlist[1] == 'alias\n' assert histlist[2] == 'alias create s shortcuts\n' + def test_history_populates_readline(hist_file): # - create a cmd2 with persistent history app = cmd2.Cmd(persistent_history_file=hist_file) @@ -718,11 +798,13 @@ def test_history_populates_readline(hist_file): # so we check to make sure that cmd2 populated the readline history # using the same rules from cmd2.rl_utils import readline + assert readline.get_current_history_length() == 3 assert readline.get_history_item(1) == 'help' assert readline.get_history_item(2) == 'shortcuts' assert readline.get_history_item(3) == 'alias' + # # test cmd2's ability to write out history on exit # we are testing the _persist_history_on_exit() method, and @@ -737,6 +819,7 @@ def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys): assert not out assert not err + def test_persist_history_permission_error(hist_file, mocker, capsys): app = cmd2.Cmd(persistent_history_file=hist_file) run_cmd(app, 'help') diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 2eccec7c..379ee2c7 100755 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -16,12 +16,14 @@ def parser(): parser = StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], - aliases={'helpalias': 'help', - '42': 'theanswer', - 'l': '!ls -al', - 'anothermultiline': 'multiline', - 'fake': 'run_pyscript'}, - shortcuts={'?': 'help', '!': 'shell'} + aliases={ + 'helpalias': 'help', + '42': 'theanswer', + 'l': '!ls -al', + 'anothermultiline': 'multiline', + 'fake': 'run_pyscript', + }, + shortcuts={'?': 'help', '!': 'shell'}, ) return parser @@ -68,34 +70,40 @@ def test_parse_empty_string_default(default_parser): assert statement.argv == statement.arg_list -@pytest.mark.parametrize('line,tokens', [ - ('command', ['command']), - (constants.COMMENT_CHAR + 'comment', []), - ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), - ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('termbare& > /tmp/output', ['termbare&', '>', '/tmp/output']), - ('help|less', ['help', '|', 'less']), -]) +@pytest.mark.parametrize( + 'line,tokens', + [ + ('command', ['command']), + (constants.COMMENT_CHAR + 'comment', []), + ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), + ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('termbare& > /tmp/output', ['termbare&', '>', '/tmp/output']), + ('help|less', ['help', '|', 'less']), + ], +) def test_tokenize_default(default_parser, line, tokens): tokens_to_test = default_parser.tokenize(line) assert tokens_to_test == tokens -@pytest.mark.parametrize('line,tokens', [ - ('command', ['command']), - ('# comment', []), - ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), - ('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']), - ('l', ['shell', 'ls', '-al']), - ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), - ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']), - ('help|less', ['help', '|', 'less']), - ('l|less', ['shell', 'ls', '-al', '|', 'less']), -]) +@pytest.mark.parametrize( + 'line,tokens', + [ + ('command', ['command']), + ('# comment', []), + ('not ' + constants.COMMENT_CHAR + ' a comment', ['not', constants.COMMENT_CHAR, 'a', 'comment']), + ('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']), + ('l', ['shell', 'ls', '-al']), + ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']), + ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']), + ('help|less', ['help', '|', 'less']), + ('l|less', ['shell', 'ls', '-al', '|', 'less']), + ], +) def test_tokenize(parser, line, tokens): tokens_to_test = parser.tokenize(line) assert tokens_to_test == tokens @@ -106,22 +114,16 @@ def test_tokenize_unclosed_quotes(parser): _ = parser.tokenize('command with "unclosed quotes') -@pytest.mark.parametrize('tokens,command,args', [ - ([], '', ''), - (['command'], 'command', ''), - (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2') -]) +@pytest.mark.parametrize( + 'tokens,command,args', [([], '', ''), (['command'], 'command', ''), (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')] +) def test_command_and_args(parser, tokens, command, args): (parsed_command, parsed_args) = parser._command_and_args(tokens) assert command == parsed_command assert args == parsed_args -@pytest.mark.parametrize('line', [ - 'plainword', - '"one word"', - "'one word'", -]) +@pytest.mark.parametrize('line', ['plainword', '"one word"', "'one word'",]) def test_parse_single_word(parser, line): statement = parser.parse(line) assert statement.command == line @@ -139,12 +141,9 @@ def test_parse_single_word(parser, line): assert statement.command_and_args == line -@pytest.mark.parametrize('line,terminator', [ - ('termbare;', ';'), - ('termbare ;', ';'), - ('termbare&', '&'), - ('termbare &', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', [('termbare;', ';'), ('termbare ;', ';'), ('termbare&', '&'), ('termbare &', '&'),] +) def test_parse_word_plus_terminator(parser, line, terminator): statement = parser.parse(line) assert statement.command == 'termbare' @@ -155,12 +154,10 @@ def test_parse_word_plus_terminator(parser, line, terminator): assert statement.expanded_command_line == statement.command + statement.terminator -@pytest.mark.parametrize('line,terminator', [ - ('termbare; suffx', ';'), - ('termbare ;suffx', ';'), - ('termbare& suffx', '&'), - ('termbare &suffx', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [('termbare; suffx', ';'), ('termbare ;suffx', ';'), ('termbare& suffx', '&'), ('termbare &suffx', '&'),], +) def test_parse_suffix_after_terminator(parser, line, terminator): statement = parser.parse(line) assert statement.command == 'termbare' @@ -224,10 +221,7 @@ def test_parse_embedded_comment_char(parser): assert statement.arg_list == statement.argv[1:] -@pytest.mark.parametrize('line', [ - 'simple | piped', - 'simple|piped', -]) +@pytest.mark.parametrize('line', ['simple | piped', 'simple|piped',]) def test_parse_simple_pipe(parser, line): statement = parser.parse(line) assert statement.command == 'simple' @@ -263,12 +257,9 @@ def test_parse_complex_pipe(parser): assert statement.pipe_to == 'piped' -@pytest.mark.parametrize('line,output', [ - ('help > out.txt', '>'), - ('help>out.txt', '>'), - ('help >> out.txt', '>>'), - ('help>>out.txt', '>>'), -]) +@pytest.mark.parametrize( + 'line,output', [('help > out.txt', '>'), ('help>out.txt', '>'), ('help >> out.txt', '>>'), ('help>>out.txt', '>>'),] +) def test_parse_redirect(parser, line, output): statement = parser.parse(line) assert statement.command == 'help' @@ -279,10 +270,7 @@ def test_parse_redirect(parser, line, output): assert statement.expanded_command_line == statement.command + ' ' + statement.output + ' ' + statement.output_to -@pytest.mark.parametrize('dest', [ - 'afile.txt', # without dashes - 'python-cmd2/afile.txt', # with dashes in path -]) +@pytest.mark.parametrize('dest', ['afile.txt', 'python-cmd2/afile.txt',]) # without dashes # with dashes in path def test_parse_redirect_with_args(parser, dest): line = 'output into > {}'.format(dest) statement = parser.parse(line) @@ -482,16 +470,19 @@ def test_parse_redirect_inside_terminator(parser): assert statement.terminator == ';' -@pytest.mark.parametrize('line,terminator', [ - ('multiline with | inside;', ';'), - ('multiline with | inside ;', ';'), - ('multiline with | inside;;;', ';'), - ('multiline with | inside;; ;;', ';'), - ('multiline with | inside&', '&'), - ('multiline with | inside &;', '&'), - ('multiline with | inside&&;', '&'), - ('multiline with | inside &; &;', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline with | inside;', ';'), + ('multiline with | inside ;', ';'), + ('multiline with | inside;;;', ';'), + ('multiline with | inside;; ;;', ';'), + ('multiline with | inside&', '&'), + ('multiline with | inside &;', '&'), + ('multiline with | inside&&;', '&'), + ('multiline with | inside &; &;', '&'), + ], +) def test_parse_multiple_terminators(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -527,13 +518,16 @@ def test_parse_basic_multiline_command(parser): assert statement.terminator == '\n' -@pytest.mark.parametrize('line,terminator', [ - ('multiline has > inside;', ';'), - ('multiline has > inside;;;', ';'), - ('multiline has > inside;; ;;', ';'), - ('multiline has > inside &', '&'), - ('multiline has > inside & &', '&'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline has > inside;', ';'), + ('multiline has > inside;;;', ';'), + ('multiline has > inside;; ;;', ';'), + ('multiline has > inside &', '&'), + ('multiline has > inside & &', '&'), + ], +) def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -556,14 +550,17 @@ def test_parse_multiline_terminated_by_empty_line(parser): assert statement.terminator == '\n' -@pytest.mark.parametrize('line,terminator', [ - ('multiline command "with\nembedded newline";', ';'), - ('multiline command "with\nembedded newline";;;', ';'), - ('multiline command "with\nembedded newline";; ;;', ';'), - ('multiline command "with\nembedded newline" &', '&'), - ('multiline command "with\nembedded newline" & &', '&'), - ('multiline command "with\nembedded newline"\n\n', '\n'), -]) +@pytest.mark.parametrize( + 'line,terminator', + [ + ('multiline command "with\nembedded newline";', ';'), + ('multiline command "with\nembedded newline";;;', ';'), + ('multiline command "with\nembedded newline";; ;;', ';'), + ('multiline command "with\nembedded newline" &', '&'), + ('multiline command "with\nembedded newline" & &', '&'), + ('multiline command "with\nembedded newline"\n\n', '\n'), + ], +) def test_parse_multiline_with_embedded_newline(parser, line, terminator): statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -633,15 +630,18 @@ def test_empty_statement_raises_exception(): app._complete_statement(' ') -@pytest.mark.parametrize('line,command,args', [ - ('helpalias', 'help', ''), - ('helpalias mycommand', 'help', 'mycommand'), - ('42', 'theanswer', ''), - ('42 arg1 arg2', 'theanswer', 'arg1 arg2'), - ('!ls', 'shell', 'ls'), - ('!ls -al /tmp', 'shell', 'ls -al /tmp'), - ('l', 'shell', 'ls -al') -]) +@pytest.mark.parametrize( + 'line,command,args', + [ + ('helpalias', 'help', ''), + ('helpalias mycommand', 'help', 'mycommand'), + ('42', 'theanswer', ''), + ('42 arg1 arg2', 'theanswer', 'arg1 arg2'), + ('!ls', 'shell', 'ls'), + ('!ls -al /tmp', 'shell', 'ls -al /tmp'), + ('l', 'shell', 'ls -al'), + ], +) def test_parse_alias_and_shortcut_expansion(parser, line, command, args): statement = parser.parse(line) assert statement.command == command @@ -659,12 +659,10 @@ def test_parse_alias_on_multiline_command(parser): assert statement.terminator == '' -@pytest.mark.parametrize('line,output', [ - ('helpalias > out.txt', '>'), - ('helpalias>out.txt', '>'), - ('helpalias >> out.txt', '>>'), - ('helpalias>>out.txt', '>>'), -]) +@pytest.mark.parametrize( + 'line,output', + [('helpalias > out.txt', '>'), ('helpalias>out.txt', '>'), ('helpalias >> out.txt', '>>'), ('helpalias>>out.txt', '>>'),], +) def test_parse_alias_redirection(parser, line, output): statement = parser.parse(line) assert statement.command == 'help' @@ -674,10 +672,7 @@ def test_parse_alias_redirection(parser, line, output): assert statement.output_to == 'out.txt' -@pytest.mark.parametrize('line', [ - 'helpalias | less', - 'helpalias|less', -]) +@pytest.mark.parametrize('line', ['helpalias | less', 'helpalias|less',]) def test_parse_alias_pipe(parser, line): statement = parser.parse(line) assert statement.command == 'help' @@ -686,14 +681,9 @@ def test_parse_alias_pipe(parser, line): assert statement.pipe_to == 'less' -@pytest.mark.parametrize('line', [ - 'helpalias;', - 'helpalias;;', - 'helpalias;; ;', - 'helpalias ;', - 'helpalias ; ;', - 'helpalias ;; ;', -]) +@pytest.mark.parametrize( + 'line', ['helpalias;', 'helpalias;;', 'helpalias;; ;', 'helpalias ;', 'helpalias ; ;', 'helpalias ;; ;',] +) def test_parse_alias_terminator_no_whitespace(parser, line): statement = parser.parse(line) assert statement.command == 'help' @@ -789,16 +779,19 @@ def test_parse_command_only_quoted_args(parser): assert statement.output_to == '' -@pytest.mark.parametrize('line,args', [ - ('helpalias > out.txt', '> out.txt'), - ('helpalias>out.txt', '>out.txt'), - ('helpalias >> out.txt', '>> out.txt'), - ('helpalias>>out.txt', '>>out.txt'), - ('help|less', '|less'), - ('helpalias;', ';'), - ('help ;;', ';;'), - ('help; ;;', '; ;;'), -]) +@pytest.mark.parametrize( + 'line,args', + [ + ('helpalias > out.txt', '> out.txt'), + ('helpalias>out.txt', '>out.txt'), + ('helpalias >> out.txt', '>> out.txt'), + ('helpalias>>out.txt', '>>out.txt'), + ('help|less', '|less'), + ('helpalias;', ';'), + ('help ;;', ';;'), + ('help; ;;', '; ;;'), + ], +) def test_parse_command_only_specialchars(parser, line, args): statement = parser.parse_command_only(line) assert statement == args @@ -814,19 +807,7 @@ def test_parse_command_only_specialchars(parser, line, args): assert statement.output_to == '' -@pytest.mark.parametrize('line', [ - '', - ';', - ';;', - ';; ;', - '&', - '& &', - ' && &', - '>', - "'", - '"', - '|', -]) +@pytest.mark.parametrize('line', ['', ';', ';;', ';; ;', '&', '& &', ' && &', '>', "'", '"', '|',]) def test_parse_command_only_empty(parser, line): statement = parser.parse_command_only(line) assert statement == '' @@ -940,6 +921,7 @@ def test_is_valid_command_valid(parser): def test_macro_normal_arg_pattern(): # This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side from cmd2.parsing import MacroArg + pattern = MacroArg.macro_normal_arg_pattern # Valid strings @@ -991,6 +973,7 @@ def test_macro_normal_arg_pattern(): def test_macro_escaped_arg_pattern(): # This pattern matches digits surrounded by 2 or more braces on both sides from cmd2.parsing import MacroArg + pattern = MacroArg.macro_escaped_arg_pattern # Valid strings diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 279f2f79..6f2b2f32 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -18,9 +18,9 @@ except ImportError: from unittest import mock - class Plugin: """A mixin class for testing hook registration and calling""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.reset_counters() @@ -222,14 +222,16 @@ class Plugin: self.called_cmdfinalization += 1 raise ValueError - def cmdfinalization_hook_system_exit(self, data: cmd2.plugin.CommandFinalizationData) -> \ - cmd2.plugin.CommandFinalizationData: + def cmdfinalization_hook_system_exit( + self, data: cmd2.plugin.CommandFinalizationData + ) -> cmd2.plugin.CommandFinalizationData: """A command finalization hook which raises a SystemExit""" self.called_cmdfinalization += 1 raise SystemExit - def cmdfinalization_hook_keyboard_interrupt(self, data: cmd2.plugin.CommandFinalizationData) -> \ - cmd2.plugin.CommandFinalizationData: + def cmdfinalization_hook_keyboard_interrupt( + self, data: cmd2.plugin.CommandFinalizationData + ) -> cmd2.plugin.CommandFinalizationData: """A command finalization hook which raises a KeyboardInterrupt""" self.called_cmdfinalization += 1 raise KeyboardInterrupt @@ -238,8 +240,9 @@ class Plugin: """A command finalization hook with no parameters.""" pass - def cmdfinalization_hook_too_many_parameters(self, one: plugin.CommandFinalizationData, two: str) -> \ - plugin.CommandFinalizationData: + def cmdfinalization_hook_too_many_parameters( + self, one: plugin.CommandFinalizationData, two: str + ) -> plugin.CommandFinalizationData: """A command finalization hook with too many parameters.""" return one @@ -262,6 +265,7 @@ class Plugin: class PluggedApp(Plugin, cmd2.Cmd): """A sample app with a plugin mixed in""" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -281,6 +285,7 @@ class PluggedApp(Plugin, cmd2.Cmd): """Repeat back the arguments""" self.poutput(namespace.cmd2_statement.get()) + ### # # test pre and postloop hooks @@ -291,11 +296,13 @@ def test_register_preloop_hook_too_many_parameters(): with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_too_many_parameters) + def test_register_preloop_hook_with_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation) + def test_preloop_hook(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -309,6 +316,7 @@ def test_preloop_hook(capsys): assert out == 'one\nhello\n' assert not err + def test_preloop_hooks(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -323,16 +331,19 @@ def test_preloop_hooks(capsys): assert out == 'one\ntwo\nhello\n' assert not err + def test_register_postloop_hook_too_many_parameters(): app = PluggedApp() with pytest.raises(TypeError): app.register_postloop_hook(app.prepost_hook_too_many_parameters) + def test_register_postloop_hook_with_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation) + def test_postloop_hook(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -346,6 +357,7 @@ def test_postloop_hook(capsys): assert out == 'hello\none\n' assert not err + def test_postloop_hooks(capsys): # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog", "say hello", 'quit'] @@ -360,6 +372,7 @@ def test_postloop_hooks(capsys): assert out == 'hello\none\ntwo\n' assert not err + ### # # test preparse hook @@ -374,6 +387,7 @@ def test_preparse(capsys): assert not err assert app.called_preparse == 1 + ### # # test postparsing hooks @@ -384,26 +398,31 @@ def test_postparsing_hook_too_many_parameters(): with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_too_many_parameters) + def test_postparsing_hook_undeclared_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_annotation) + def test_postparsing_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_parameter_annotation) + def test_postparsing_hook_undeclared_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_return_annotation) + def test_postparsing_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_return_annotation) + def test_postparsing_hook(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -429,6 +448,7 @@ def test_postparsing_hook(capsys): assert not err assert app.called_postparsing == 2 + def test_postparsing_hook_stop_first(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_stop) @@ -443,6 +463,7 @@ def test_postparsing_hook_stop_first(capsys): assert app.called_postparsing == 1 assert stop + def test_postparsing_hook_stop_second(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) @@ -464,6 +485,7 @@ def test_postparsing_hook_stop_second(capsys): assert app.called_postparsing == 2 assert stop + def test_postparsing_hook_emptystatement_first(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_emptystatement) @@ -484,6 +506,7 @@ def test_postparsing_hook_emptystatement_first(capsys): assert not err assert app.called_postparsing == 1 + def test_postparsing_hook_emptystatement_second(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) @@ -514,6 +537,7 @@ def test_postparsing_hook_emptystatement_second(capsys): assert not err assert app.called_postparsing == 2 + def test_postparsing_hook_exception(capsys): app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_exception) @@ -534,6 +558,7 @@ def test_postparsing_hook_exception(capsys): assert err assert app.called_postparsing == 1 + ### # # test precmd hooks @@ -546,26 +571,31 @@ def test_register_precmd_hook_parameter_count(): with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_too_many_parameters) + def test_register_precmd_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_parameter_annotation) + def test_register_precmd_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_parameter_annotation) + def test_register_precmd_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_return_annotation) + def test_register_precmd_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_return_annotation) + def test_precmd_hook(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -594,6 +624,7 @@ def test_precmd_hook(capsys): # with two hooks registered, we should get precmd() and both hooks assert app.called_precmd == 3 + def test_precmd_hook_emptystatement_first(capsys): app = PluggedApp() app.register_precmd_hook(app.precmd_hook_emptystatement) @@ -619,6 +650,7 @@ def test_precmd_hook_emptystatement_first(capsys): # called assert app.called_precmd == 1 + def test_precmd_hook_emptystatement_second(capsys): app = PluggedApp() app.register_precmd_hook(app.precmd_hook) @@ -655,6 +687,7 @@ def test_precmd_hook_emptystatement_second(capsys): # if a registered hook throws an exception, precmd() is never called assert app.called_precmd == 2 + ### # # test postcmd hooks @@ -667,26 +700,31 @@ def test_register_postcmd_hook_parameter_count(): with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_too_many_parameters) + def test_register_postcmd_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_parameter_annotation) + def test_register_postcmd_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_parameter_annotation) + def test_register_postcmd_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_return_annotation) + def test_register_postcmd_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_return_annotation) + def test_postcmd(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -715,6 +753,7 @@ def test_postcmd(capsys): # with two hooks registered, we should get precmd() and both hooks assert app.called_postcmd == 3 + def test_postcmd_exception_first(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook_exception) @@ -741,6 +780,7 @@ def test_postcmd_exception_first(capsys): # called assert app.called_postcmd == 1 + def test_postcmd_exception_second(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) @@ -766,6 +806,7 @@ def test_postcmd_exception_second(capsys): # the exception assert app.called_postcmd == 2 + ## # # command finalization @@ -778,26 +819,31 @@ def test_register_cmdfinalization_hook_parameter_count(): with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_too_many_parameters) + def test_register_cmdfinalization_hook_no_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_parameter_annotation) + def test_register_cmdfinalization_hook_wrong_parameter_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_parameter_annotation) + def test_register_cmdfinalization_hook_no_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_return_annotation) + def test_register_cmdfinalization_hook_wrong_return_annotation(): app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_return_annotation) + def test_cmdfinalization(capsys): app = PluggedApp() app.onecmd_plus_hooks('say hello') @@ -822,6 +868,7 @@ def test_cmdfinalization(capsys): assert not err assert app.called_cmdfinalization == 2 + def test_cmdfinalization_stop_first(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop) @@ -833,6 +880,7 @@ def test_cmdfinalization_stop_first(capsys): assert app.called_cmdfinalization == 2 assert stop + def test_cmdfinalization_stop_second(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook) @@ -844,6 +892,7 @@ def test_cmdfinalization_stop_second(capsys): assert app.called_cmdfinalization == 2 assert stop + def test_cmdfinalization_hook_exception(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_exception) @@ -864,6 +913,7 @@ def test_cmdfinalization_hook_exception(capsys): assert err assert app.called_cmdfinalization == 1 + def test_cmdfinalization_hook_system_exit(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit) @@ -871,6 +921,7 @@ def test_cmdfinalization_hook_system_exit(capsys): assert stop assert app.called_cmdfinalization == 1 + def test_cmdfinalization_hook_keyboard_interrupt(capsys): app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt) @@ -893,6 +944,7 @@ def test_cmdfinalization_hook_keyboard_interrupt(capsys): assert stop assert app.called_cmdfinalization == 1 + def test_skip_postcmd_hooks(capsys): app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) @@ -905,6 +957,7 @@ def test_skip_postcmd_hooks(capsys): assert app.called_postcmd == 0 assert app.called_cmdfinalization == 1 + def test_cmd2_argparse_exception(capsys): """ Verify Cmd2ArgparseErrors raised after calling a command prevent postcmd events from diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index 8cfd8578..e0b2b3c5 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -20,11 +20,13 @@ except ImportError: HOOK_OUTPUT = "TEST_OUTPUT" + def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData: """A cmdfinalization_hook hook which requests application exit""" print(HOOK_OUTPUT) return data + def test_run_pyscript(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') @@ -33,6 +35,7 @@ def test_run_pyscript(base_app, request): out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert expected in out + def test_run_pyscript_recursive_not_allowed(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'recursive.py') @@ -41,11 +44,13 @@ def test_run_pyscript_recursive_not_allowed(base_app, request): out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert err[0] == expected + def test_run_pyscript_with_nonexist_file(base_app): python_script = 'does_not_exist.py' out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) assert "Error reading script file" in err[0] + def test_run_pyscript_with_non_python_file(base_app, request): m = mock.MagicMock(name='input', return_value='2') builtins.input = m @@ -55,6 +60,7 @@ def test_run_pyscript_with_non_python_file(base_app, request): out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename)) assert "does not have a .py extension" in err[0] + @pytest.mark.parametrize('python_script', odd_file_names) def test_run_pyscript_with_odd_file_names(base_app, python_script): """ @@ -69,6 +75,7 @@ def test_run_pyscript_with_odd_file_names(base_app, python_script): err = ''.join(err) assert "Error reading script file '{}'".format(python_script) in err + def test_run_pyscript_with_exception(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'raises_exception.py') @@ -76,10 +83,12 @@ def test_run_pyscript_with_exception(base_app, request): assert err[0].startswith('Traceback') assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1] + def test_run_pyscript_requires_an_argument(base_app): out, err = run_cmd(base_app, "run_pyscript") assert "the following arguments are required: script_path" in err[1] + def test_run_pyscript_help(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'help.py') @@ -87,6 +96,7 @@ def test_run_pyscript_help(base_app, request): out2, err2 = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) assert out1 and out1 == out2 + def test_run_pyscript_dir(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py') @@ -94,6 +104,7 @@ def test_run_pyscript_dir(base_app, request): out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) assert out[0] == "['cmd_echo']" + def test_run_pyscript_stdout_capture(base_app, request): base_app.register_cmdfinalization_hook(cmdfinalization_hook) test_dir = os.path.dirname(request.module.__file__) @@ -103,6 +114,7 @@ def test_run_pyscript_stdout_capture(base_app, request): assert out[0] == "PASSED" assert out[1] == "PASSED" + def test_run_pyscript_stop(base_app, request): # Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop test_dir = os.path.dirname(request.module.__file__) @@ -117,6 +129,7 @@ def test_run_pyscript_stop(base_app, request): stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script)) assert stop + def test_run_pyscript_environment(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'environment.py') @@ -124,7 +137,8 @@ def test_run_pyscript_environment(base_app, request): assert out[0] == "PASSED" -def test_run_pyscript_app_echo(base_app, request): + +def test_run_pyscript_app_echo(base_app, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'echo.py') out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index 0d2edfb2..c83aee2c 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -46,15 +46,30 @@ def test_column_creation(): def test_column_alignment(): - column_1 = Column("Col 1", width=10, - header_horiz_align=HorizontalAlignment.LEFT, header_vert_align=VerticalAlignment.TOP, - data_horiz_align=HorizontalAlignment.LEFT, data_vert_align=VerticalAlignment.TOP) - column_2 = Column("Col 2", width=10, - header_horiz_align=HorizontalAlignment.CENTER, header_vert_align=VerticalAlignment.MIDDLE, - data_horiz_align=HorizontalAlignment.CENTER, data_vert_align=VerticalAlignment.MIDDLE) - column_3 = Column("Col 3", width=10, - header_horiz_align=HorizontalAlignment.RIGHT, header_vert_align=VerticalAlignment.BOTTOM, - data_horiz_align=HorizontalAlignment.RIGHT, data_vert_align=VerticalAlignment.BOTTOM) + column_1 = Column( + "Col 1", + width=10, + header_horiz_align=HorizontalAlignment.LEFT, + header_vert_align=VerticalAlignment.TOP, + data_horiz_align=HorizontalAlignment.LEFT, + data_vert_align=VerticalAlignment.TOP, + ) + column_2 = Column( + "Col 2", + width=10, + header_horiz_align=HorizontalAlignment.CENTER, + header_vert_align=VerticalAlignment.MIDDLE, + data_horiz_align=HorizontalAlignment.CENTER, + data_vert_align=VerticalAlignment.MIDDLE, + ) + column_3 = Column( + "Col 3", + width=10, + header_horiz_align=HorizontalAlignment.RIGHT, + header_vert_align=VerticalAlignment.BOTTOM, + data_horiz_align=HorizontalAlignment.RIGHT, + data_vert_align=VerticalAlignment.BOTTOM, + ) column_4 = Column("Three\nline\nheader", width=10) columns = [column_1, column_2, column_3, column_4] @@ -68,16 +83,20 @@ def test_column_alignment(): # Create a header row header = tc.generate_row() - assert header == ('Col 1 Three \n' - ' Col 2 line \n' - ' Col 3 header ') + assert header == ( + 'Col 1 Three \n' + ' Col 2 line \n' + ' Col 3 header ' + ) # Create a data row row_data = ["Val 1", "Val 2", "Val 3", "Three\nline\ndata"] row = tc.generate_row(row_data=row_data) - assert row == ('Val 1 Three \n' - ' Val 2 line \n' - ' Val 3 data ') + assert row == ( + 'Val 1 Three \n' + ' Val 2 line \n' + ' Val 3 data ' + ) def test_wrap_text(): @@ -87,19 +106,12 @@ def test_wrap_text(): # Test normal wrapping row_data = ['Some text to wrap\nA new line that will wrap\nNot wrap\n 1 2 3'] row = tc.generate_row(row_data=row_data) - assert row == ('Some text \n' - 'to wrap \n' - 'A new line\n' - 'that will \n' - 'wrap \n' - 'Not wrap \n' - ' 1 2 3 ') + assert row == ('Some text \n' 'to wrap \n' 'A new line\n' 'that will \n' 'wrap \n' 'Not wrap \n' ' 1 2 3 ') # Test preserving a multiple space sequence across a line break row_data = ['First last one'] row = tc.generate_row(row_data=row_data) - assert row == ('First \n' - ' last one ') + assert row == ('First \n' ' last one ') def test_wrap_text_max_lines(): @@ -109,32 +121,27 @@ def test_wrap_text_max_lines(): # Test not needing to truncate the final line row_data = ['First line last line'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last line ') + assert row == ('First line\n' 'last line ') # Test having to truncate the last word because it's too long for the final line row_data = ['First line last lineextratext'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last lineβ¦') + assert row == ('First line\n' 'last lineβ¦') # Test having to truncate the last word because it fits the final line but there is more text not being included row_data = ['First line thistxtfit extra'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'thistxtfiβ¦') + assert row == ('First line\n' 'thistxtfiβ¦') # Test having to truncate the last word because it fits the final line but there are more lines not being included row_data = ['First line thistxtfit\nextra'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'thistxtfiβ¦') + assert row == ('First line\n' 'thistxtfiβ¦') # Test having space left on the final line and adding an ellipsis because there are more lines not being included row_data = ['First line last line\nextra line'] row = tc.generate_row(row_data=row_data) - assert row == ('First line\n' - 'last lineβ¦') + assert row == ('First line\n' 'last lineβ¦') def test_wrap_long_word(): @@ -147,8 +154,7 @@ def test_wrap_long_word(): # Test header row header = tc.generate_row() - assert header == ('LongColumn \n' - 'Name Col 2 ') + assert header == ('LongColumn \n' 'Name Col 2 ') # Test data row row_data = list() @@ -160,9 +166,22 @@ def test_wrap_long_word(): row_data.append("Word LongerThan10") row = tc.generate_row(row_data=row_data) - expected = (ansi.RESET_ALL + ansi.fg.green + "LongerThan" + ansi.RESET_ALL + " Word \n" - + ansi.RESET_ALL + ansi.fg.green + "10" + ansi.fg.reset + ansi.RESET_ALL + ' ' + ansi.RESET_ALL + ' LongerThan\n' - ' 10 ') + expected = ( + ansi.RESET_ALL + + ansi.fg.green + + "LongerThan" + + ansi.RESET_ALL + + " Word \n" + + ansi.RESET_ALL + + ansi.fg.green + + "10" + + ansi.fg.reset + + ansi.RESET_ALL + + ' ' + + ansi.RESET_ALL + + ' LongerThan\n' + ' 10 ' + ) assert row == expected @@ -191,8 +210,7 @@ def test_wrap_long_word_max_data_lines(): row_data.append("A LongerThan10RunsOverLast") row = tc.generate_row(row_data=row_data) - assert row == ('LongerThan LongerThan LongerThan A LongerTβ¦\n' - '10FitsLast 10FitsLasβ¦ 10RunsOveβ¦ ') + assert row == ('LongerThan LongerThan LongerThan A LongerTβ¦\n' '10FitsLast 10FitsLasβ¦ 10RunsOveβ¦ ') def test_wrap_long_char_wider_than_max_width(): @@ -235,8 +253,7 @@ def test_tabs(): column_2 = Column("Col 2") tc = TableCreator([column_1, column_2], tab_width=2) - row = tc.generate_row(fill_char='\t', pre_line='\t', - inter_cell='\t', post_line='\t') + row = tc.generate_row(fill_char='\t', pre_line='\t', inter_cell='\t', post_line='\t') assert row == ' Col 1 Col 2 ' @@ -252,67 +269,74 @@ def test_simple_table_creation(): st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '----------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Custom divider st = SimpleTable([column_1, column_2], divider_char='β') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'ββββββββββββββββββββββββββββββββββ\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + 'ββββββββββββββββββββββββββββββββββ\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # No divider st = SimpleTable([column_1, column_2], divider_char=None) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ' + ) # No row spacing st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, row_spacing=0) - assert table == ('Col 1 Col 2 \n' - '----------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # No header st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, include_header=False) - assert table == ('Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ('Col 1 Row 1 Col 2 Row 1 \n' '\n' 'Col 1 Row 2 Col 2 Row 2 ') # Wide custom divider (divider needs no padding) st = SimpleTable([column_1, column_2], divider_char='ζ·±') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + 'ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Wide custom divider (divider needs padding) column_2 = Column("Col 2", width=17) st = SimpleTable([column_1, column_2], divider_char='ζ·±') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·± \n' - 'Col 1 Row 1 Col 2 Row 1 \n' - '\n' - 'Col 1 Row 2 Col 2 Row 2 ') + assert table == ( + 'Col 1 Col 2 \n' + 'ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·±ζ·± \n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ' + ) # Invalid divider character with pytest.raises(TypeError) as excinfo: @@ -363,44 +387,52 @@ def test_bordered_table_creation(): # Default options bt = BorderedTable([column_1, column_2]) table = bt.generate_table(row_data) - assert table == ('βββββββββββββββββββ€ββββββββββββββββββ\n' - 'β Col 1 β Col 2 β\n' - 'β ββββββββββββββββββͺββββββββββββββββββ£\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - 'βββββββββββββββββββΌββββββββββββββββββ’\n' - 'β Col 1 Row 2 β Col 2 Row 2 β\n' - 'βββββββββββββββββββ§ββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββ€ββββββββββββββββββ\n' + 'β Col 1 β Col 2 β\n' + 'β ββββββββββββββββββͺββββββββββββββββββ£\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + 'βββββββββββββββββββΌββββββββββββββββββ’\n' + 'β Col 1 Row 2 β Col 2 Row 2 β\n' + 'βββββββββββββββββββ§ββββββββββββββββββ' + ) # No column borders bt = BorderedTable([column_1, column_2], column_borders=False) table = bt.generate_table(row_data) - assert table == ('ββββββββββββββββββββββββββββββββββββ\n' - 'β Col 1 Col 2 β\n' - 'β βββββββββββββββββββββββββββββββββββ£\n' - 'β Col 1 Row 1 Col 2 Row 1 β\n' - 'ββββββββββββββββββββββββββββββββββββ’\n' - 'β Col 1 Row 2 Col 2 Row 2 β\n' - 'ββββββββββββββββββββββββββββββββββββ') + assert table == ( + 'ββββββββββββββββββββββββββββββββββββ\n' + 'β Col 1 Col 2 β\n' + 'β βββββββββββββββββββββββββββββββββββ£\n' + 'β Col 1 Row 1 Col 2 Row 1 β\n' + 'ββββββββββββββββββββββββββββββββββββ’\n' + 'β Col 1 Row 2 Col 2 Row 2 β\n' + 'ββββββββββββββββββββββββββββββββββββ' + ) # No header bt = BorderedTable([column_1, column_2]) table = bt.generate_table(row_data, include_header=False) - assert table == ('βββββββββββββββββββ€ββββββββββββββββββ\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - 'βββββββββββββββββββΌββββββββββββββββββ’\n' - 'β Col 1 Row 2 β Col 2 Row 2 β\n' - 'βββββββββββββββββββ§ββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββ€ββββββββββββββββββ\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + 'βββββββββββββββββββΌββββββββββββββββββ’\n' + 'β Col 1 Row 2 β Col 2 Row 2 β\n' + 'βββββββββββββββββββ§ββββββββββββββββββ' + ) # Non-default padding bt = BorderedTable([column_1, column_2], padding=2) table = bt.generate_table(row_data) - assert table == ('βββββββββββββββββββββ€ββββββββββββββββββββ\n' - 'β Col 1 β Col 2 β\n' - 'β ββββββββββββββββββββͺββββββββββββββββββββ£\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - 'βββββββββββββββββββββΌββββββββββββββββββββ’\n' - 'β Col 1 Row 2 β Col 2 Row 2 β\n' - 'βββββββββββββββββββββ§ββββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββββ€ββββββββββββββββββββ\n' + 'β Col 1 β Col 2 β\n' + 'β ββββββββββββββββββββͺββββββββββββββββββββ£\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + 'βββββββββββββββββββββΌββββββββββββββββββββ’\n' + 'β Col 1 Row 2 β Col 2 Row 2 β\n' + 'βββββββββββββββββββββ§ββββββββββββββββββββ' + ) # Invalid padding with pytest.raises(ValueError) as excinfo: @@ -457,50 +489,60 @@ def test_alternating_table_creation(): # Default options at = AlternatingTable([column_1, column_2]) table = at.generate_table(row_data) - assert table == ('βββββββββββββββββββ€ββββββββββββββββββ\n' - 'β Col 1 β Col 2 β\n' - 'β ββββββββββββββββββͺββββββββββββββββββ£\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' - 'βββββββββββββββββββ§ββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββ€ββββββββββββββββββ\n' + 'β Col 1 β Col 2 β\n' + 'β ββββββββββββββββββͺββββββββββββββββββ£\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' + 'βββββββββββββββββββ§ββββββββββββββββββ' + ) # Other bg colors at = AlternatingTable([column_1, column_2], bg_odd=ansi.bg.bright_blue, bg_even=ansi.bg.green) table = at.generate_table(row_data) - assert table == ('βββββββββββββββββββ€ββββββββββββββββββ\n' - 'β Col 1 β Col 2 β\n' - 'β ββββββββββββββββββͺββββββββββββββββββ£\n' - '\x1b[104mβ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m β \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m β\x1b[49m\n' - '\x1b[42mβ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m β \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m β\x1b[49m\n' - 'βββββββββββββββββββ§ββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββ€ββββββββββββββββββ\n' + 'β Col 1 β Col 2 β\n' + 'β ββββββββββββββββββͺββββββββββββββββββ£\n' + '\x1b[104mβ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m β \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m β\x1b[49m\n' + '\x1b[42mβ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m β \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m β\x1b[49m\n' + 'βββββββββββββββββββ§ββββββββββββββββββ' + ) # No column borders at = AlternatingTable([column_1, column_2], column_borders=False) table = at.generate_table(row_data) - assert table == ('ββββββββββββββββββββββββββββββββββββ\n' - 'β Col 1 Col 2 β\n' - 'β βββββββββββββββββββββββββββββββββββ£\n' - 'β Col 1 Row 1 Col 2 Row 1 β\n' - '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' - 'ββββββββββββββββββββββββββββββββββββ') + assert table == ( + 'ββββββββββββββββββββββββββββββββββββ\n' + 'β Col 1 Col 2 β\n' + 'β βββββββββββββββββββββββββββββββββββ£\n' + 'β Col 1 Row 1 Col 2 Row 1 β\n' + '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' + 'ββββββββββββββββββββββββββββββββββββ' + ) # No header at = AlternatingTable([column_1, column_2]) table = at.generate_table(row_data, include_header=False) - assert table == ('βββββββββββββββββββ€ββββββββββββββββββ\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' - 'βββββββββββββββββββ§ββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββ€ββββββββββββββββββ\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' + 'βββββββββββββββββββ§ββββββββββββββββββ' + ) # Non-default padding at = AlternatingTable([column_1, column_2], padding=2) table = at.generate_table(row_data) - assert table == ('βββββββββββββββββββββ€ββββββββββββββββββββ\n' - 'β Col 1 β Col 2 β\n' - 'β ββββββββββββββββββββͺββββββββββββββββββββ£\n' - 'β Col 1 Row 1 β Col 2 Row 1 β\n' - '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' - 'βββββββββββββββββββββ§ββββββββββββββββββββ') + assert table == ( + 'βββββββββββββββββββββ€ββββββββββββββββββββ\n' + 'β Col 1 β Col 2 β\n' + 'β ββββββββββββββββββββͺββββββββββββββββββββ£\n' + 'β Col 1 Row 1 β Col 2 Row 1 β\n' + '\x1b[100mβ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m β\x1b[49m\n' + 'βββββββββββββββββββββ§ββββββββββββββββββββ' + ) # Invalid padding with pytest.raises(ValueError) as excinfo: diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 55d60e18..5b19e11e 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -46,7 +46,7 @@ class CmdLineApp(cmd2.Cmd): """Repeats what you tell me to.""" arg = ' '.join(arg) if opts.piglatin: - arg = '%s%say' % (arg[1:], arg[0]) + arg = '{}{}ay'.format(arg[1:], arg[0]) if opts.shout: arg = arg.upper() repetitions = opts.repeat or 1 @@ -66,16 +66,16 @@ class CmdLineApp(cmd2.Cmd): def do_mumble(self, opts, arg): """Mumbles what you tell me to.""" repetitions = opts.repeat or 1 - #arg = arg.split() + # arg = arg.split() for _ in range(min(repetitions, self.maxrepeats)): output = [] - if random.random() < .33: + if random.random() < 0.33: output.append(random.choice(self.MUMBLE_FIRST)) for word in arg: - if random.random() < .40: + if random.random() < 0.40: output.append(random.choice(self.MUMBLES)) output.append(word) - if random.random() < .25: + if random.random() < 0.25: output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) @@ -98,23 +98,27 @@ def test_commands_at_invocation(): out = app.stdout.getvalue() assert out == expected -@pytest.mark.parametrize('filename,feedback_to_output', [ - ('bol_eol.txt', False), - ('characterclass.txt', False), - ('dotstar.txt', False), - ('extension_notation.txt', False), - ('from_cmdloop.txt', True), - ('multiline_no_regex.txt', False), - ('multiline_regex.txt', False), - ('no_output.txt', False), - ('no_output_last.txt', False), - ('regex_set.txt', False), - ('singleslash.txt', False), - ('slashes_escaped.txt', False), - ('slashslash.txt', False), - ('spaces.txt', False), - ('word_boundaries.txt', False), - ]) + +@pytest.mark.parametrize( + 'filename,feedback_to_output', + [ + ('bol_eol.txt', False), + ('characterclass.txt', False), + ('dotstar.txt', False), + ('extension_notation.txt', False), + ('from_cmdloop.txt', True), + ('multiline_no_regex.txt', False), + ('multiline_regex.txt', False), + ('no_output.txt', False), + ('no_output_last.txt', False), + ('regex_set.txt', False), + ('singleslash.txt', False), + ('slashes_escaped.txt', False), + ('slashslash.txt', False), + ('spaces.txt', False), + ('word_boundaries.txt', False), + ], +) def test_transcript(request, capsys, filename, feedback_to_output): # Get location of the transcript test_dir = os.path.dirname(request.module.__file__) @@ -141,6 +145,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert err.startswith(expected_start) assert err.endswith(expected_end) + def test_history_transcript(): app = CmdLineApp() app.stdout = StdSim(app.stdout) @@ -168,6 +173,7 @@ this is a \/multiline\/ command assert xscript == expected + def test_history_transcript_bad_filename(): app = CmdLineApp() app.stdout = StdSim(app.stdout) @@ -247,27 +253,30 @@ def test_generate_transcript_stop(capsys): assert err.startswith("Interrupting this command\nCommand 2 triggered a stop") -@pytest.mark.parametrize('expected, transformed', [ - # strings with zero or one slash or with escaped slashes means no regular - # expression present, so the result should just be what re.escape returns. - # we don't use static strings in these tests because re.escape behaves - # differently in python 3.7 than in prior versions - ( 'text with no slashes', re.escape('text with no slashes') ), - ( 'specials .*', re.escape('specials .*') ), - ( 'use 2/3 cup', re.escape('use 2/3 cup') ), - ( '/tmp is nice', re.escape('/tmp is nice') ), - ( 'slash at end/', re.escape('slash at end/') ), - # escaped slashes - ( r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/' ) ), - # regexes - ( '/.*/', '.*' ), - ( 'specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+' ), - ( r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more') ), - ( r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /') ), - # inception: slashes in our regex. backslashed on input, bare on output - ( r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /') ), - ( r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff') ), - ]) +@pytest.mark.parametrize( + 'expected, transformed', + [ + # strings with zero or one slash or with escaped slashes means no regular + # expression present, so the result should just be what re.escape returns. + # we don't use static strings in these tests because re.escape behaves + # differently in python 3.7 than in prior versions + ('text with no slashes', re.escape('text with no slashes')), + ('specials .*', re.escape('specials .*')), + ('use 2/3 cup', re.escape('use 2/3 cup')), + ('/tmp is nice', re.escape('/tmp is nice')), + ('slash at end/', re.escape('slash at end/')), + # escaped slashes + (r'not this slash\/ or this one\/', re.escape('not this slash/ or this one/')), + # regexes + ('/.*/', '.*'), + ('specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+'), + (r'/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more')), + (r'not \/, use /\|?/, not \/', re.escape('not /, use ') + r'\|?' + re.escape(', not /')), + # inception: slashes in our regex. backslashed on input, bare on output + (r'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /')), + (r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff')), + ], +) def test_parse_transcript_expected(expected, transformed): app = CmdLineApp() diff --git a/tests/test_utils.py b/tests/test_utils.py index 5336ccfd..383ea6d7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -26,37 +26,44 @@ def test_strip_quotes_no_quotes(): stripped = cu.strip_quotes(base_str) assert base_str == stripped + def test_strip_quotes_with_quotes(): base_str = '"' + HELLO_WORLD + '"' stripped = cu.strip_quotes(base_str) assert stripped == HELLO_WORLD + def test_remove_duplicates_no_duplicates(): no_dups = [5, 4, 3, 2, 1] assert cu.remove_duplicates(no_dups) == no_dups + def test_remove_duplicates_with_duplicates(): duplicates = [1, 1, 2, 3, 9, 9, 7, 8] assert cu.remove_duplicates(duplicates) == [1, 2, 3, 9, 7, 8] + def test_unicode_normalization(): s1 = 'cafΓ©' s2 = 'cafe\u0301' assert s1 != s2 assert cu.norm_fold(s1) == cu.norm_fold(s2) + def test_unicode_casefold(): micro = 'Β΅' micro_cf = micro.casefold() assert micro != micro_cf assert cu.norm_fold(micro) == cu.norm_fold(micro_cf) + def test_alphabetical_sort(): my_list = ['cafΓ©', 'Β΅', 'A', 'micro', 'unity', 'cafeteria'] assert cu.alphabetical_sort(my_list) == ['A', 'cafeteria', 'cafΓ©', 'micro', 'unity', 'Β΅'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.alphabetical_sort(my_list) == ['a1', 'A11', 'A2', 'a22', 'a3'] + def test_try_int_or_force_to_lower_case(): str1 = '17' assert cu.try_int_or_force_to_lower_case(str1) == 17 @@ -67,6 +74,7 @@ def test_try_int_or_force_to_lower_case(): str1 = '' assert cu.try_int_or_force_to_lower_case(str1) == '' + def test_natural_keys(): my_list = ['cafΓ©', 'Β΅', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] my_list.sort(key=cu.natural_keys) @@ -75,24 +83,28 @@ def test_natural_keys(): my_list.sort(key=cu.natural_keys) assert my_list == ['a1', 'A2', 'a3', 'A11', 'a22'] + def test_natural_sort(): my_list = ['cafΓ©', 'Β΅', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] assert cu.natural_sort(my_list) == ['A', 'cafΓ©', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'Β΅'] my_list = ['a3', 'a22', 'A2', 'A11', 'a1'] assert cu.natural_sort(my_list) == ['a1', 'A2', 'a3', 'A11', 'a22'] + def test_is_quoted_short(): my_str = '' assert not cu.is_quoted(my_str) your_str = '"' assert not cu.is_quoted(your_str) + def test_is_quoted_yes(): my_str = '"This is a test"' assert cu.is_quoted(my_str) your_str = "'of the emergengy broadcast system'" assert cu.is_quoted(your_str) + def test_is_quoted_no(): my_str = '"This is a test' assert not cu.is_quoted(my_str) @@ -101,6 +113,7 @@ def test_is_quoted_no(): simple_str = "hello world" assert not cu.is_quoted(simple_str) + def test_quote_string(): my_str = "Hello World" assert cu.quote_string(my_str) == '"' + my_str + '"' @@ -111,12 +124,14 @@ def test_quote_string(): my_str = '"Hello World"' assert cu.quote_string(my_str) == "'" + my_str + "'" + def test_quote_string_if_needed_yes(): my_str = "Hello World" assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"' your_str = '"foo" bar' assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'" + def test_quote_string_if_needed_no(): my_str = "HelloWorld" assert cu.quote_string_if_needed(my_str) == my_str @@ -135,22 +150,26 @@ def test_stdsim_write_str(stdout_sim): stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str + def test_stdsim_write_bytes(stdout_sim): b_str = b'Hello World' with pytest.raises(TypeError): stdout_sim.write(b_str) + def test_stdsim_buffer_write_bytes(stdout_sim): b_str = b'Hello World' stdout_sim.buffer.write(b_str) assert stdout_sim.getvalue() == b_str.decode() assert stdout_sim.getbytes() == b_str + def test_stdsim_buffer_write_str(stdout_sim): my_str = 'Hello World' with pytest.raises(TypeError): stdout_sim.buffer.write(my_str) + def test_stdsim_read(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) @@ -176,6 +195,7 @@ def test_stdsim_read_bytes(stdout_sim): assert stdout_sim.readbytes() == b_str assert stdout_sim.getbytes() == b'' + def test_stdsim_clear(stdout_sim): my_str = 'Hello World' stdout_sim.write(my_str) @@ -183,6 +203,7 @@ def test_stdsim_clear(stdout_sim): stdout_sim.clear() assert stdout_sim.getvalue() == '' + def test_stdsim_getattr_exist(stdout_sim): # Here the StdSim getattr is allowing us to access methods within StdSim my_str = 'Hello World' @@ -190,10 +211,12 @@ def test_stdsim_getattr_exist(stdout_sim): val_func = getattr(stdout_sim, 'getvalue') assert val_func() == my_str + def test_stdsim_getattr_noexist(stdout_sim): # Here the StdSim getattr is allowing us to access methods defined by the inner stream assert not stdout_sim.isatty() + def test_stdsim_pause_storage(stdout_sim): # Test pausing storage for string data my_str = 'Hello World' @@ -217,11 +240,13 @@ def test_stdsim_pause_storage(stdout_sim): stdout_sim.buffer.write(b_str) assert stdout_sim.getbytes() == b'' + def test_stdsim_line_buffering(base_app): # This exercises the case of writing binary data that contains new lines/carriage returns to a StdSim # when line buffering is on. The output should immediately be flushed to the underlying stream. import os import tempfile + file = tempfile.NamedTemporaryFile(mode='wt') file.line_buffering = True @@ -256,6 +281,7 @@ def pr_none(): pr = cu.ProcReader(proc, None, None) return pr + def test_proc_reader_send_sigint(pr_none): assert pr_none._proc.poll() is None pr_none.send_sigint() @@ -274,6 +300,7 @@ def test_proc_reader_send_sigint(pr_none): else: assert ret_code == -signal.SIGINT + def test_proc_reader_terminate(pr_none): assert pr_none._proc.poll() is None pr_none.terminate() @@ -297,11 +324,13 @@ def test_proc_reader_terminate(pr_none): def context_flag(): return cu.ContextFlag() + def test_context_flag_bool(context_flag): assert not context_flag with context_flag: assert context_flag + def test_context_flag_exit_err(context_flag): with pytest.raises(ValueError): context_flag.__exit__() @@ -313,30 +342,35 @@ def test_truncate_line(): truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + def test_truncate_line_already_fits(): line = 'long' max_width = 4 truncated = cu.truncate_line(line, max_width) assert truncated == line + def test_truncate_line_with_newline(): line = 'fo\no' max_width = 2 with pytest.raises(ValueError): cu.truncate_line(line, max_width) + def test_truncate_line_width_is_too_small(): line = 'foo' max_width = 0 with pytest.raises(ValueError): cu.truncate_line(line, max_width) + def test_truncate_line_wide_text(): line = 'θΉθΉother' max_width = 6 truncated = cu.truncate_line(line, max_width) assert truncated == 'θΉθΉo' + HORIZONTAL_ELLIPSIS + def test_truncate_line_split_wide_text(): """Test when truncation results in a string which is shorter than max_width""" line = '1θΉ2θΉ' @@ -344,12 +378,14 @@ def test_truncate_line_split_wide_text(): truncated = cu.truncate_line(line, max_width) assert truncated == '1' + HORIZONTAL_ELLIPSIS + def test_truncate_line_tabs(): line = 'has\ttab' max_width = 9 truncated = cu.truncate_line(line, max_width) assert truncated == 'has t' + HORIZONTAL_ELLIPSIS + def test_truncate_with_style(): from cmd2 import ansi @@ -374,6 +410,7 @@ def test_truncate_with_style(): truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + after_style + def test_align_text_fill_char_is_tab(): text = 'foo' fill_char = '\t' @@ -381,6 +418,7 @@ def test_align_text_fill_char_is_tab(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + ' ' + def test_align_text_with_style(): from cmd2 import ansi @@ -422,8 +460,8 @@ def test_align_text_with_style(): line_1_text = ansi.fg.bright_blue + 'line1' line_2_text = ansi.fg.bright_blue + 'line2' + ansi.FG_RESET - assert aligned == (left_fill + line_1_text + right_fill + '\n' + - left_fill + line_2_text + right_fill) + assert aligned == (left_fill + line_1_text + right_fill + '\n' + left_fill + line_2_text + right_fill) + def test_align_text_width_is_too_small(): text = 'foo' @@ -432,6 +470,7 @@ def test_align_text_width_is_too_small(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_fill_char_is_too_long(): text = 'foo' fill_char = 'fill' @@ -439,6 +478,7 @@ def test_align_text_fill_char_is_too_long(): with pytest.raises(TypeError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_fill_char_is_newline(): text = 'foo' fill_char = '\n' @@ -446,6 +486,7 @@ def test_align_text_fill_char_is_newline(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_has_tabs(): text = '\t\tfoo' fill_char = '-' @@ -453,6 +494,7 @@ def test_align_text_has_tabs(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=2) assert aligned == ' ' + 'foo' + '---' + def test_align_text_blank(): text = '' fill_char = '-' @@ -460,6 +502,7 @@ def test_align_text_blank(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == fill_char * width + def test_align_text_wider_than_width(): text = 'long text field' fill_char = '-' @@ -467,6 +510,7 @@ def test_align_text_wider_than_width(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + def test_align_text_wider_than_width_truncate(): text = 'long text field' fill_char = '-' @@ -474,6 +518,7 @@ def test_align_text_wider_than_width_truncate(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == 'long te' + HORIZONTAL_ELLIPSIS + def test_align_text_wider_than_width_truncate_add_fill(): """Test when truncation results in a string which is shorter than width and align_text adds filler""" text = '1θΉ2θΉ' @@ -482,6 +527,7 @@ def test_align_text_wider_than_width_truncate_add_fill(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char + def test_align_text_has_unprintable(): text = 'foo\x02' fill_char = '-' @@ -489,9 +535,12 @@ def test_align_text_has_unprintable(): with pytest.raises(ValueError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + def test_align_text_term_width(): import shutil + from cmd2 import ansi + text = 'foo' fill_char = ' ' @@ -501,6 +550,7 @@ def test_align_text_term_width(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char) assert aligned == text + expected_fill + def test_align_left(): text = 'foo' fill_char = '-' @@ -508,13 +558,14 @@ def test_align_left(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char + def test_align_left_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_left(text, fill_char=fill_char, width=width) - assert aligned == ('foo----\n' - 'shoes--') + assert aligned == ('foo----\n' 'shoes--') + def test_align_left_wide_text(): text = 'θΉ' @@ -523,6 +574,7 @@ def test_align_left_wide_text(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char + def test_align_left_wide_fill(): text = 'foo' fill_char = 'θΉ' @@ -530,6 +582,7 @@ def test_align_left_wide_fill(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + def test_align_left_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' @@ -538,6 +591,7 @@ def test_align_left_wide_fill_needs_padding(): aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + ' ' + def test_align_center(): text = 'foo' fill_char = '-' @@ -545,13 +599,14 @@ def test_align_center(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_center(text, fill_char=fill_char, width=width) - assert aligned == ('--foo--\n' - '-shoes-') + assert aligned == ('--foo--\n' '-shoes-') + def test_align_center_wide_text(): text = 'θΉ' @@ -560,6 +615,7 @@ def test_align_center_wide_text(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_wide_fill(): text = 'foo' fill_char = 'θΉ' @@ -567,6 +623,7 @@ def test_align_center_wide_fill(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + def test_align_center_wide_fill_needs_right_padding(): """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' @@ -575,6 +632,7 @@ def test_align_center_wide_fill_needs_right_padding(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + ' ' + def test_align_center_wide_fill_needs_left_and_right_padding(): """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' @@ -583,6 +641,7 @@ def test_align_center_wide_fill_needs_left_and_right_padding(): aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text + fill_char + ' ' + def test_align_right(): text = 'foo' fill_char = '-' @@ -590,13 +649,14 @@ def test_align_right(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text + def test_align_right_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 aligned = cu.align_right(text, fill_char=fill_char, width=width) - assert aligned == ('----foo\n' - '--shoes') + assert aligned == ('----foo\n' '--shoes') + def test_align_right_wide_text(): text = 'θΉ' @@ -605,6 +665,7 @@ def test_align_right_wide_text(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text + def test_align_right_wide_fill(): text = 'foo' fill_char = 'θΉ' @@ -612,6 +673,7 @@ def test_align_right_wide_fill(): aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + def test_align_right_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' @@ -627,26 +689,31 @@ def test_str_to_bool_true(): assert cu.str_to_bool('TRUE') assert cu.str_to_bool('tRuE') + def test_str_to_bool_false(): assert not cu.str_to_bool('false') assert not cu.str_to_bool('False') assert not cu.str_to_bool('FALSE') assert not cu.str_to_bool('fAlSe') + def test_str_to_bool_invalid(): with pytest.raises(ValueError): cu.str_to_bool('other') + def test_str_to_bool_bad_input(): with pytest.raises(ValueError): cu.str_to_bool(1) + def test_find_editor_specified(): expected_editor = os.path.join('fake_dir', 'editor') with mock.patch.dict(os.environ, {'EDITOR': expected_editor}): editor = cu.find_editor() assert editor == expected_editor + def test_find_editor_not_specified(): # Use existing path env setting. Something in the editor list should be found. editor = cu.find_editor() diff --git a/tests/test_utils_defining_class.py b/tests/test_utils_defining_class.py index 0fbcf83b..5d667678 100644 --- a/tests/test_utils_defining_class.py +++ b/tests/test_utils_defining_class.py @@ -77,7 +77,7 @@ def test_get_defining_class(): assert cu.get_defining_class(partial_unbound) is ParentClass assert cu.get_defining_class(nested_partial_unbound) is ParentClass - partial_bound = functools.partial(parent_instance.parent_only_func, 1) - nested_partial_bound = functools.partial(partial_bound, 2) + partial_bound = functools.partial(parent_instance.parent_only_func, 1) + nested_partial_bound = functools.partial(partial_bound, 2) assert cu.get_defining_class(partial_bound) is ParentClass assert cu.get_defining_class(nested_partial_bound) is ParentClass diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index 037be199..d6968575 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -7,10 +7,10 @@ from contextlib import redirect_stderr, redirect_stdout from typing import List, Optional, Union from unittest import mock +from cmd2_ext_test import ExternalTestMixin from pytest import fixture import cmd2 -from cmd2_ext_test import ExternalTestMixin from cmd2.utils import StdSim # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) @@ -26,9 +26,9 @@ except ImportError: pass -def verify_help_text(cmd2_app: cmd2.Cmd, - help_output: Union[str, List[str]], - verbose_strings: Optional[List[str]] = None) -> None: +def verify_help_text( + cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None +) -> None: """This function verifies that all expected commands are present in the help text. :param cmd2_app: instance of cmd2.Cmd @@ -157,12 +157,7 @@ def base_app(): # These are odd file names for testing quoting of them -odd_file_names = [ - 'nothingweird', - 'has spaces', - '"is_double_quoted"', - "'is_single_quoted'" -] +odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: @@ -181,6 +176,7 @@ def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Opti Matches are stored in app.completion_matches These matches also have been sorted by complete() """ + def get_line(): return line @@ -199,6 +195,7 @@ def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Opti class WithCommandSets(ExternalTestMixin, cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" + def __init__(self, *args, **kwargs): super(WithCommandSets, self).__init__(*args, **kwargs) diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py index bd2bed42..d71c3670 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -9,7 +9,8 @@ import argparse import pytest import cmd2 -from .conftest import run_cmd, WithCommandSets + +from .conftest import WithCommandSets, run_cmd class SubcommandSet(cmd2.CommandSet): @@ -65,8 +66,7 @@ class SubcommandSet(cmd2.CommandSet): @pytest.fixture def subcommand_app(): - app = WithCommandSets(auto_load_commands=False, - command_sets=[SubcommandSet(1)]) + app = WithCommandSets(auto_load_commands=False, command_sets=[SubcommandSet(1)]) return app diff --git a/tests_isolated/test_commandset/test_categories.py b/tests_isolated/test_commandset/test_categories.py index c266e0d4..ae31d6a1 100644 --- a/tests_isolated/test_commandset/test_categories.py +++ b/tests_isolated/test_commandset/test_categories.py @@ -12,6 +12,7 @@ from cmd2 import CommandSet, with_default_category @with_default_category('Default Category') class MyBaseCommandSet(CommandSet): """Defines a default category for all sub-class CommandSets""" + def __init__(self, _: Any): super(MyBaseCommandSet, self).__init__() @@ -20,6 +21,7 @@ class ChildInheritsParentCategories(MyBaseCommandSet): """ This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category' """ + def do_hello(self, _: cmd2.Statement): self._cmd.poutput('Hello') @@ -33,6 +35,7 @@ class ChildOverridesParentCategoriesNonHeritable(MyBaseCommandSet): This subclass overrides the 'Default Category' from the parent, but in a non-heritable fashion. Sub-classes of this CommandSet will not inherit this category and will, instead, inherit 'Default Category' """ + def do_goodbye(self, _: cmd2.Statement): self._cmd.poutput('Goodbye') @@ -42,6 +45,7 @@ class GrandchildInheritsGrandparentCategory(ChildOverridesParentCategoriesNonHer This subclass's parent class declared its default category non-heritable. Instead, it inherits the category defined by the grandparent class. """ + def do_aloha(self, _: cmd2.Statement): self._cmd.poutput('Aloha') @@ -52,6 +56,7 @@ class ChildOverridesParentCategories(MyBaseCommandSet): This subclass is decorated with a default category that is heritable. This overrides the parent class's default category declaration. """ + def do_bonjour(self, _: cmd2.Statement): self._cmd.poutput('Bonjour') @@ -61,6 +66,7 @@ class GrandchildInheritsHeritable(ChildOverridesParentCategories): This subclass's parent declares a default category that overrides its parent. As a result, commands in this CommandSet will be categorized under 'Heritable Category' """ + def do_monde(self, _: cmd2.Statement): self._cmd.poutput('Monde') diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 21cce8bf..b6761642 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -11,9 +11,10 @@ import pytest import cmd2 from cmd2 import utils -from .conftest import complete_tester, WithCommandSets from cmd2.exceptions import CommandSetRegistrationError +from .conftest import WithCommandSets, complete_tester + class CommandSetBase(cmd2.CommandSet): pass @@ -51,9 +52,8 @@ class CommandSetA(CommandSetBase): def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) if unknown and len(unknown): - self._cmd.poutput('Unknown: ' + ', '.join(['{}']*len(unknown)).format(*unknown)) - self._cmd.last_result = {'arg1': ns.arg1, - 'unknown': unknown} + self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown)) + self._cmd.last_result = {'arg1': ns.arg1, 'unknown': unknown} def help_cranberry(self): self._cmd.stdout.write('This command does diddly squat...\n') @@ -63,7 +63,7 @@ class CommandSetA(CommandSetBase): def do_durian(self, args: List[str]): """Durian Command""" self._cmd.poutput('{} Arguments: '.format(len(args))) - self._cmd.poutput(', '.join(['{}']*len(args)).format(*args)) + self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args)) self._cmd.last_result = {'args': args} def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: @@ -567,6 +567,7 @@ def test_subcommands(command_sets_manual): command_sets_manual.unregister_command_set(veg_cmds) command_sets_manual.unregister_command_set(base_cmds) + def test_nested_subcommands(command_sets_manual): base_cmds = LoadableBase(1) pasta_cmds = LoadablePastaStir(1) @@ -614,6 +615,7 @@ def test_nested_subcommands(command_sets_manual): class AppWithSubCommands(cmd2.Cmd): """Class for testing usage of `as_subcommand_to` decorator directly in a Cmd2 subclass.""" + def __init__(self, *args, **kwargs): super(AppWithSubCommands, self).__init__(*args, **kwargs) |