diff --git a/CHANGELOG.md b/CHANGELOG.md index 3423fe707..7e98f405f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Breaking Change - `cmd2` 2.6 supports Python 3.9+ (removed support for Python 3.8) + - Renamed methods in `cmd2.ansi.Cursor` to make it clear they are intended for internal use only as was documented - Enhancements - Add support for Python 3.14 diff --git a/Makefile b/Makefile index de2a86955..52de0b91d 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,18 @@ check: ## Run code quality tools. @echo "🚀 Static type checking: Running mypy" @uv run mypy +.PHONY: format +format: ## Perform ruff formatting + @uv run ruff format + +.PHONY: lint +lint: ## Perform ruff linting + @uv run ruff check --fix + +.PHONY: typecheck +typecheck: ## Perform type checking + @uv run mypy + .PHONY: test test: ## Test the code with pytest. @echo "🚀 Testing code: Running pytest" diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 8f1f030ea..e9c9a5c6d 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -1,11 +1,8 @@ -# -# -*- coding: utf-8 -*- -# flake8: noqa F401 """This simply imports certain things for backwards compatibility.""" -import sys - +import argparse import importlib.metadata as importlib_metadata +import sys try: __version__ = importlib_metadata.version(__name__) @@ -13,14 +10,12 @@ # package is not installed pass -from typing import List - from .ansi import ( - Cursor, Bg, - Fg, + Cursor, EightBitBg, EightBitFg, + Fg, RgbBg, RgbFg, TextStyle, @@ -36,20 +31,18 @@ # Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER. # Do this before loading cmd2.Cmd class so its commands use the custom 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) +from . import plugin from .argparse_completer import set_default_ap_completer_type - 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_category, as_subcommand_to +from .decorators import as_subcommand_to, with_argparser, with_argument_list, with_category from .exceptions import ( Cmd2ArgparseError, CommandSetRegistrationError, @@ -57,13 +50,11 @@ PassThroughException, SkipPostcommandHooks, ) -from . import plugin from .parsing import Statement from .py_bridge import CommandResult -from .utils import categorize, CompletionMode, CustomCompletionSettings, Settable - +from .utils import CompletionMode, CustomCompletionSettings, Settable, categorize -__all__: List[str] = [ +__all__: list[str] = [ 'COMMAND_NAME', 'DEFAULT_SHORTCUTS', # ANSI Exports diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 8242957ae..22497f4ed 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -1,6 +1,4 @@ -# coding=utf-8 -""" -Support for ANSI escape sequences which are used for things like applying style to text, +"""Support for ANSI escape sequences which are used for things like applying style to text, setting the window title, and asynchronous alerts. """ @@ -12,7 +10,6 @@ from typing import ( IO, Any, - List, Optional, cast, ) @@ -31,18 +28,18 @@ class AllowStyle(Enum): - """Values for ``cmd2.ansi.allow_style``""" + """Values for ``cmd2.ansi.allow_style``.""" ALWAYS = 'Always' # Always output ANSI style sequences NEVER = 'Never' # Remove ANSI style sequences from all output TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal def __str__(self) -> str: - """Return value instead of enum name for printing in cmd2's set command""" + """Return value instead of enum name for printing in cmd2's set command.""" return str(self.value) def __repr__(self) -> str: - """Return quoted value instead of enum description for printing in cmd2's set command""" + """Return quoted value instead of enum description for printing in cmd2's set command.""" return repr(self.value) @@ -85,8 +82,7 @@ def __repr__(self) -> str: def strip_style(text: str) -> str: - """ - Strip ANSI style sequences from a string. + """Strip ANSI style sequences from a string. :param text: string which may contain ANSI style sequences :return: the same string with any ANSI style sequences removed @@ -95,8 +91,7 @@ def strip_style(text: str) -> str: def style_aware_wcswidth(text: str) -> int: - """ - Wrap wcswidth to make it compatible with strings that contain ANSI style sequences. + """Wrap wcswidth to make it compatible with strings that contain ANSI style sequences. This is intended for single line strings. If text contains a newline, this function will return -1. For multiline strings, call widest_line() instead. @@ -110,8 +105,7 @@ def style_aware_wcswidth(text: str) -> int: def widest_line(text: str) -> int: - """ - Return the width of the widest line in a multiline string. This wraps style_aware_wcswidth() + """Return the width of the widest line in a multiline string. This wraps style_aware_wcswidth() so it handles ANSI style sequences and has the same restrictions on non-printable characters. :param text: the string being measured @@ -130,8 +124,7 @@ def widest_line(text: str) -> int: def style_aware_write(fileobj: IO[str], msg: str) -> None: - """ - Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting + """Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting. :param fileobj: the file object being written to :param msg: the string being written @@ -145,8 +138,7 @@ def style_aware_write(fileobj: IO[str], msg: str) -> None: # Utility functions which create various ANSI sequences #################################################################################### def set_title(title: str) -> str: - """ - Generate a string that, when printed, sets a terminal's window title. + """Generate a string that, when printed, sets a terminal's window title. :param title: new title for the window :return: the set title string @@ -155,8 +147,7 @@ def set_title(title: str) -> str: def clear_screen(clear_type: int = 2) -> str: - """ - Generate a string that, when printed, clears a terminal screen based on value of clear_type. + """Generate a string that, when printed, clears a terminal screen based on value of clear_type. :param clear_type: integer which specifies how to clear the screen (Defaults to 2) Possible values: @@ -173,8 +164,7 @@ def clear_screen(clear_type: int = 2) -> str: def clear_line(clear_type: int = 2) -> str: - """ - Generate a string that, when printed, clears a line based on value of clear_type. + """Generate a string that, when printed, clears a line based on value of clear_type. :param clear_type: integer which specifies how to clear the line (Defaults to 2) Possible values: @@ -193,69 +183,63 @@ def clear_line(clear_type: int = 2) -> str: # Base classes which are not intended to be used directly #################################################################################### class AnsiSequence: - """Base class to create ANSI sequence strings""" + """Base class to create ANSI sequence strings.""" def __add__(self, other: Any) -> str: - """ - Support building an ANSI sequence string when self is the left operand - e.g. Fg.LIGHT_MAGENTA + "hello" + """Support building an ANSI sequence string when self is the left operand + e.g. Fg.LIGHT_MAGENTA + "hello". """ return str(self) + str(other) def __radd__(self, other: Any) -> str: - """ - Support building an ANSI sequence string when self is the right operand - e.g. "hello" + Fg.RESET + """Support building an ANSI sequence string when self is the right operand + e.g. "hello" + Fg.RESET. """ return str(other) + str(self) class FgColor(AnsiSequence): - """Base class for ANSI Sequences which set foreground text color""" - - pass + """Base class for ANSI Sequences which set foreground text color.""" class BgColor(AnsiSequence): - """Base class for ANSI Sequences which set background text color""" - - pass + """Base class for ANSI Sequences which set background text color.""" #################################################################################### -# Implementations intended for direct use +# Implementations intended for direct use (do NOT use outside of cmd2) #################################################################################### class Cursor: - """Create ANSI sequences to alter the cursor position""" + """Create ANSI sequences to alter the cursor position.""" @staticmethod - def UP(count: int = 1) -> str: - """Move the cursor up a specified amount of lines (Defaults to 1)""" + def _up(count: int = 1) -> str: + """Move the cursor up a specified amount of lines (Defaults to 1).""" return f"{CSI}{count}A" @staticmethod - def DOWN(count: int = 1) -> str: - """Move the cursor down a specified amount of lines (Defaults to 1)""" + def _down(count: int = 1) -> str: + """Move the cursor down a specified amount of lines (Defaults to 1).""" return f"{CSI}{count}B" @staticmethod - def FORWARD(count: int = 1) -> str: - """Move the cursor forward a specified amount of lines (Defaults to 1)""" + def _forward(count: int = 1) -> str: + """Move the cursor forward a specified amount of lines (Defaults to 1).""" return f"{CSI}{count}C" @staticmethod - def BACK(count: int = 1) -> str: - """Move the cursor back a specified amount of lines (Defaults to 1)""" + def _back(count: int = 1) -> str: + """Move the cursor back a specified amount of lines (Defaults to 1).""" return f"{CSI}{count}D" @staticmethod - def SET_POS(x: int, y: int) -> str: - """Set the cursor position to coordinates which are 1-based""" + def _set_pos(x: int, y: int) -> str: + """Set the cursor position to coordinates which are 1-based.""" return f"{CSI}{y};{x}H" class TextStyle(AnsiSequence, Enum): - """Create text style ANSI sequences""" + """Create text style ANSI sequences.""" # Resets all styles and colors of text RESET_ALL = 0 @@ -278,17 +262,15 @@ class TextStyle(AnsiSequence, Enum): UNDERLINE_DISABLE = 24 def __str__(self) -> str: - """ - Return ANSI text style sequence instead of enum name + """Return ANSI text style sequence instead of enum name This is helpful when using a TextStyle in an f-string or format() call - e.g. my_str = f"{TextStyle.UNDERLINE_ENABLE}hello{TextStyle.UNDERLINE_DISABLE}" + e.g. my_str = f"{TextStyle.UNDERLINE_ENABLE}hello{TextStyle.UNDERLINE_DISABLE}". """ return f"{CSI}{self.value}m" class Fg(FgColor, Enum): - """ - Create ANSI sequences for the 16 standard terminal foreground text colors. + """Create ANSI sequences for the 16 standard terminal foreground text colors. A terminal's color settings affect how these colors appear. To reset any foreground color, use Fg.RESET. """ @@ -313,17 +295,15 @@ class Fg(FgColor, Enum): RESET = 39 def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using an Fg in an f-string or format() call - e.g. my_str = f"{Fg.BLUE}hello{Fg.RESET}" + e.g. my_str = f"{Fg.BLUE}hello{Fg.RESET}". """ return f"{CSI}{self.value}m" class Bg(BgColor, Enum): - """ - Create ANSI sequences for the 16 standard terminal background text colors. + """Create ANSI sequences for the 16 standard terminal background text colors. A terminal's color settings affect how these colors appear. To reset any background color, use Bg.RESET. """ @@ -348,17 +328,15 @@ class Bg(BgColor, Enum): RESET = 49 def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using a Bg in an f-string or format() call - e.g. my_str = f"{Bg.BLACK}hello{Bg.RESET}" + e.g. my_str = f"{Bg.BLACK}hello{Bg.RESET}". """ return f"{CSI}{self.value}m" class EightBitFg(FgColor, Enum): - """ - Create ANSI sequences for 8-bit terminal foreground text colors. Most terminals support 8-bit/256-color mode. + """Create ANSI sequences for 8-bit terminal foreground text colors. Most terminals support 8-bit/256-color mode. The first 16 colors correspond to the 16 colors from Fg and behave the same way. To reset any foreground color, including 8-bit, use Fg.RESET. """ @@ -621,17 +599,15 @@ class EightBitFg(FgColor, Enum): GRAY_93 = 255 def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using an EightBitFg in an f-string or format() call - e.g. my_str = f"{EightBitFg.SLATE_BLUE_1}hello{Fg.RESET}" + e.g. my_str = f"{EightBitFg.SLATE_BLUE_1}hello{Fg.RESET}". """ return f"{CSI}38;5;{self.value}m" class EightBitBg(BgColor, Enum): - """ - Create ANSI sequences for 8-bit terminal background text colors. Most terminals support 8-bit/256-color mode. + """Create ANSI sequences for 8-bit terminal background text colors. Most terminals support 8-bit/256-color mode. The first 16 colors correspond to the 16 colors from Bg and behave the same way. To reset any background color, including 8-bit, use Bg.RESET. """ @@ -894,23 +870,20 @@ class EightBitBg(BgColor, Enum): GRAY_93 = 255 def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using an EightBitBg in an f-string or format() call - e.g. my_str = f"{EightBitBg.KHAKI_3}hello{Bg.RESET}" + e.g. my_str = f"{EightBitBg.KHAKI_3}hello{Bg.RESET}". """ return f"{CSI}48;5;{self.value}m" class RgbFg(FgColor): - """ - Create ANSI sequences for 24-bit (RGB) terminal foreground text colors. The terminal must support 24-bit/true-color mode. + """Create ANSI sequences for 24-bit (RGB) terminal foreground text colors. The terminal must support 24-bit/true-color. To reset any foreground color, including 24-bit, use Fg.RESET. """ def __init__(self, r: int, g: int, b: int) -> None: - """ - RgbFg initializer + """RgbFg initializer. :param r: integer from 0-255 for the red component of the color :param g: integer from 0-255 for the green component of the color @@ -923,23 +896,20 @@ def __init__(self, r: int, g: int, b: int) -> None: self._sequence = f"{CSI}38;2;{r};{g};{b}m" def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using an RgbFg in an f-string or format() call - e.g. my_str = f"{RgbFg(0, 55, 100)}hello{Fg.RESET}" + e.g. my_str = f"{RgbFg(0, 55, 100)}hello{Fg.RESET}". """ return self._sequence class RgbBg(BgColor): - """ - Create ANSI sequences for 24-bit (RGB) terminal background text colors. The terminal must support 24-bit/true-color mode. + """Create ANSI sequences for 24-bit (RGB) terminal background text colors. The terminal must support 24-bit/true-color. To reset any background color, including 24-bit, use Bg.RESET. """ def __init__(self, r: int, g: int, b: int) -> None: - """ - RgbBg initializer + """RgbBg initializer. :param r: integer from 0-255 for the red component of the color :param g: integer from 0-255 for the green component of the color @@ -952,10 +922,9 @@ def __init__(self, r: int, g: int, b: int) -> None: self._sequence = f"{CSI}48;2;{r};{g};{b}m" def __str__(self) -> str: - """ - Return ANSI color sequence instead of enum name + """Return ANSI color sequence instead of enum name This is helpful when using an RgbBg in an f-string or format() call - e.g. my_str = f"{RgbBg(100, 255, 27)}hello{Bg.RESET}" + e.g. my_str = f"{RgbBg(100, 255, 27)}hello{Bg.RESET}". """ return self._sequence @@ -972,8 +941,7 @@ def style( strikethrough: Optional[bool] = None, underline: Optional[bool] = None, ) -> str: - """ - Apply ANSI colors and/or styles to a string and return it. + """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 to undo whatever styling was done at the beginning. @@ -992,11 +960,11 @@ def style( :raises TypeError: if bg isn't None or a subclass of BgColor :return: the stylized string """ - # List of strings that add style - additions: List[AnsiSequence] = [] + # list of strings that add style + additions: list[AnsiSequence] = [] - # List of strings that remove style - removals: List[AnsiSequence] = [] + # list of strings that remove style + removals: list[AnsiSequence] = [] # Process the style settings if fg is not None: @@ -1062,7 +1030,6 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off :param alert_msg: the message to display to the user :return: the correct string so that the alert message appears to the user to be printed above the current line. """ - # Split the prompt lines since it can contain newline characters. prompt_lines = prompt.splitlines() or [''] @@ -1092,11 +1059,11 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off # Move the cursor down to the last input line if cursor_input_line != num_input_terminal_lines: - terminal_str += Cursor.DOWN(num_input_terminal_lines - cursor_input_line) + terminal_str += Cursor._down(num_input_terminal_lines - cursor_input_line) # Clear each line from the bottom up so that the cursor ends up on the first prompt line total_lines = num_prompt_terminal_lines + num_input_terminal_lines - terminal_str += (clear_line() + Cursor.UP(1)) * (total_lines - 1) + terminal_str += (clear_line() + Cursor._up(1)) * (total_lines - 1) # Clear the first prompt line terminal_str += clear_line() diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 8dd543c20..420fd6447 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa C901 -# NOTE: Ignoring flake8 cyclomatic complexity in this file -""" -This module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. +"""This module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. See the header of argparse_custom.py for instructions on how to use these features. """ @@ -14,10 +10,7 @@ ) from typing import ( TYPE_CHECKING, - Dict, - List, Optional, - Type, Union, cast, ) @@ -62,28 +55,27 @@ def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str: - """Build tab completion hint for a given argument""" + """Build tab completion hint for a given argument.""" # Check if hinting is disabled for this argument suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined] if suppress_hint or arg_action.help == argparse.SUPPRESS: return '' - else: - # Use the parser's help formatter to display just this action's help text - formatter = parser._get_formatter() - formatter.start_section("Hint") - formatter.add_argument(arg_action) - formatter.end_section() - return formatter.format_help() + + # Use the parser's help formatter to display just this action's help text + formatter = parser._get_formatter() + formatter.start_section("Hint") + formatter.add_argument(arg_action) + formatter.end_section() + return formatter.format_help() def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool: - """Returns if a token is just a single flag prefix character""" + """Returns if a token is just a single flag prefix character.""" return len(token) == 1 and token[0] in parser.prefix_chars def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: - """ - Determine if a token looks like a flag. Unless an argument has nargs set to argparse.REMAINDER, + """Determine if a token looks like a flag. Unless an argument has nargs set to argparse.REMAINDER, then anything that looks like a flag can't be consumed as a value for it. Based on argparse._parse_optional(). """ @@ -96,9 +88,8 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: return False # If it looks like a negative number, it is not a flag unless there are negative-number-like flags - if parser._negative_number_matcher.match(token): - if not parser._has_negative_number_optionals: - return False + if parser._negative_number_matcher.match(token) and not parser._has_negative_number_optionals: + return False # Flags can't have a space if ' ' in token: @@ -109,7 +100,7 @@ def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool: class _ArgumentState: - """Keeps state of an argument being parsed""" + """Keeps state of an argument being parsed.""" def __init__(self, arg_action: argparse.Action) -> None: self.action = arg_action @@ -131,7 +122,7 @@ def __init__(self, arg_action: argparse.Action) -> None: elif self.action.nargs == argparse.OPTIONAL: self.min = 0 self.max = 1 - elif self.action.nargs == argparse.ZERO_OR_MORE or self.action.nargs == argparse.REMAINDER: + elif self.action.nargs in (argparse.ZERO_OR_MORE, argparse.REMAINDER): self.min = 0 self.max = INFINITY elif self.action.nargs == argparse.ONE_OR_MORE: @@ -144,38 +135,33 @@ def __init__(self, arg_action: argparse.Action) -> None: class _UnfinishedFlagError(CompletionError): def __init__(self, flag_arg_state: _ArgumentState) -> None: + """CompletionError which occurs when the user has not finished the current flag + :param flag_arg_state: information about the unfinished flag action. """ - 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(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max)), - flag_arg_state.count, - ) + arg = f'{argparse._get_action_name(flag_arg_state.action)}' + err = f'{generate_range_error(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max))}' + error = f"Error: argument {arg}: {err} ({flag_arg_state.count} entered)" super().__init__(error) class _NoResultsError(CompletionError): def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None: - """ - CompletionError which occurs when there are no results. If hinting is allowed, then its message will + """CompletionError which occurs when there are no results. If hinting is allowed, then its message will be a hint about the argument being tab completed. :param parser: ArgumentParser instance which owns the action being tab completed - :param arg_action: action being tab completed + :param arg_action: action being tab completed. """ # Set apply_style to False because we don't want hints to look like errors super().__init__(_build_hint(parser, arg_action), apply_style=False) class ArgparseCompleter: - """Automatic command line tab completion based on argparse parameters""" + """Automatic command line tab completion based on argparse parameters.""" def __init__( - self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None + self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[dict[str, list[str]]] = None ) -> None: - """ - Create an ArgparseCompleter + """Create an ArgparseCompleter. :param parser: ArgumentParser instance :param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter @@ -187,7 +173,7 @@ def __init__( self._cmd2_app = cmd2_app if parent_tokens is None: - parent_tokens = dict() + parent_tokens = {} self._parent_tokens = parent_tokens self._flags = [] # all flags in this command @@ -213,10 +199,9 @@ def __init__( self._subcommand_action = action def complete( - self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *, cmd_set: Optional[CommandSet] = None - ) -> List[str]: - """ - Complete text using argparse metadata + self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: Optional[CommandSet] = None + ) -> list[str]: + """Complete text using argparse metadata. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -245,26 +230,25 @@ def complete( flag_arg_state: Optional[_ArgumentState] = None # Non-reusable flags that we've parsed - matched_flags: List[str] = [] + matched_flags: list[str] = [] # Keeps track of arguments we've seen and any tokens they consumed - consumed_arg_values: Dict[str, List[str]] = dict() # dict(arg_name -> List[tokens]) + consumed_arg_values: dict[str, list[str]] = {} # dict(arg_name -> list[tokens]) # Completed mutually exclusive groups - completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict() + completed_mutex_groups: dict[argparse._MutuallyExclusiveGroup, argparse.Action] = {} def consume_argument(arg_state: _ArgumentState) -> None: - """Consuming token as an argument""" + """Consuming token as an argument.""" arg_state.count += 1 consumed_arg_values.setdefault(arg_state.action.dest, []) consumed_arg_values[arg_state.action.dest].append(token) def update_mutex_groups(arg_action: argparse.Action) -> None: - """ - Check if an argument belongs to a mutually exclusive group and either mark that group + """Check if an argument belongs to a mutually exclusive group and either mark that group as complete or print an error if the group has already been completed :param arg_action: the action of the argument - :raises CompletionError: if the group is already completed + :raises CompletionError: if the group is already completed. """ # Check if this action is in a mutually exclusive group for group in self._parser._mutually_exclusive_groups: @@ -277,9 +261,9 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: 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) - ) + arg_str = f'{argparse._get_action_name(arg_action)}' + completer_str = f'{argparse._get_action_name(completer_action)}' + error = f"Error: argument {arg_str}: not allowed with argument {completer_str}" raise CompletionError(error) # Mark that this action completed the group @@ -289,7 +273,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: for group_action in group._group_actions: if group_action == arg_action: continue - elif group_action in self._flag_to_action.values(): + if group_action in self._flag_to_action.values(): matched_flags.extend(group_action.option_strings) elif group_action in remaining_positionals: remaining_positionals.remove(group_action) @@ -307,15 +291,15 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: continue # If we're in a flag REMAINDER arg, force all future tokens to go to that until a double dash is hit - elif flag_arg_state is not None and flag_arg_state.is_remainder: - if token == '--': + if flag_arg_state is not None and flag_arg_state.is_remainder: + if token == '--': # noqa: S105 flag_arg_state = None else: consume_argument(flag_arg_state) continue # Handle '--' which tells argparse all remaining arguments are non-flags - elif token == '--' and not skip_remaining_flags: + if token == '--' and not skip_remaining_flags: # noqa: S105 # Check if there is an unfinished flag if ( flag_arg_state is not None @@ -325,10 +309,9 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: raise _UnfinishedFlagError(flag_arg_state) # Otherwise end the current flag - else: - flag_arg_state = None - skip_remaining_flags = True - continue + flag_arg_state = None + skip_remaining_flags = True + continue # Check the format of the current token to see if it can be an argument's value if _looks_like_flag(token, self._parser) and not skip_remaining_flags: @@ -408,13 +391,11 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: return completer.complete( text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set ) - else: - # Invalid subcommand entered, so no way to complete remaining tokens - return [] + # Invalid subcommand entered, so no way to complete remaining tokens + return [] # Otherwise keep track of the argument - else: - pos_arg_state = _ArgumentState(action) + pos_arg_state = _ArgumentState(action) # Check if we have a positional to consume this token if pos_arg_state is not None: @@ -467,7 +448,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: return completion_results # Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag - elif ( + if ( (isinstance(flag_arg_state.min, int) and flag_arg_state.count < flag_arg_state.min) or not _single_prefix_char(text, self._parser) or skip_remaining_flags @@ -493,7 +474,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: return completion_results # Otherwise, print a hint if text isn't possibly the start of a flag - elif not _single_prefix_char(text, self._parser) or skip_remaining_flags: + if not _single_prefix_char(text, self._parser) or skip_remaining_flags: raise _NoResultsError(self._parser, pos_arg_state.action) # If we aren't skipping remaining flags, then complete flag names if either is True: @@ -507,9 +488,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None: return completion_results - def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]: - """Tab completion routine for a parsers unused flags""" - + def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]: + """Tab completion routine for a parsers unused flags.""" # Build a list of flags that can be tab completed match_against = [] @@ -524,7 +504,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche matches = self._cmd2_app.basic_complete(text, line, begidx, endidx, match_against) # Build a dictionary linking actions with their matched flag names - matched_actions: Dict[argparse.Action, List[str]] = dict() + matched_actions: dict[argparse.Action, list[str]] = {} for flag in matches: action = self._flag_to_action[flag] matched_actions.setdefault(action, []) @@ -541,14 +521,13 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche return matches - def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]: - """Format CompletionItems into hint table""" - + def _format_completions(self, arg_state: _ArgumentState, completions: Union[list[str], list[CompletionItem]]) -> list[str]: + """Format CompletionItems into hint table.""" # Nothing to do if we don't have at least 2 completions which are all CompletionItems if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions): - return cast(List[str], completions) + return cast(list[str], completions) - completion_items = cast(List[CompletionItem], completions) + completion_items = cast(list[CompletionItem], completions) # Check if the data being completed have a numerical type all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items) @@ -599,7 +578,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List item.description = item.description.replace('\t', four_spaces) desc_width = max(widest_line(item.description), desc_width) - cols = list() + cols = [] dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT cols.append( Column( @@ -616,17 +595,16 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0) # Return sorted list of completions - return cast(List[str], completions) + return cast(list[str], completions) - def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]: - """ - Supports cmd2's help command in the completion of subcommand names + def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: list[str]) -> list[str]: + """Supports cmd2's help command in the completion of subcommand names :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text :param tokens: arguments passed to command/subcommand - :return: List of subcommand completions + :return: list of subcommand completions. """ # If our parser has subcommands, we must examine the tokens and check if they are subcommands # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter. @@ -638,18 +616,16 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in completer = completer_type(parser, self._cmd2_app) return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :]) - elif token_index == len(tokens) - 1: + if token_index == len(tokens) - 1: # Since this is the last token, we will attempt to complete it return self._cmd2_app.basic_complete(text, line, begidx, endidx, self._subcommand_action.choices) - else: - break + break return [] - def format_help(self, tokens: List[str]) -> str: - """ - Supports cmd2's help command in the retrieval of help text + def format_help(self, tokens: list[str]) -> str: + """Supports cmd2's help command in the retrieval of help text :param tokens: arguments passed to help command - :return: help text of the command being queried + :return: help text of the command being queried. """ # If our parser has subcommands, we must examine the tokens and check if they are subcommands # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter. @@ -661,8 +637,7 @@ def format_help(self, tokens: List[str]) -> str: completer = completer_type(parser, self._cmd2_app) return completer.format_help(tokens[token_index + 1 :]) - else: - break + break return self._parser.format_help() def _complete_arg( @@ -672,17 +647,16 @@ def _complete_arg( begidx: int, endidx: int, arg_state: _ArgumentState, - consumed_arg_values: Dict[str, List[str]], + consumed_arg_values: dict[str, list[str]], *, cmd_set: Optional[CommandSet] = None, - ) -> List[str]: - """ - Tab completion routine for an argparse argument + ) -> list[str]: + """Tab completion routine for an argparse argument :return: list of completions - :raises CompletionError: if the completer or choices function this calls raises one + :raises CompletionError: if the completer or choices function this calls raises one. """ # Check if the arg provides choices to the user - arg_choices: Union[List[str], ChoicesCallable] + arg_choices: Union[list[str], ChoicesCallable] if arg_state.action.choices is not None: arg_choices = list(arg_state.action.choices) if not arg_choices: @@ -739,7 +713,7 @@ def _complete_arg( # Otherwise use basic_complete on the choices else: # Check if the choices come from a function - completion_items: List[str] = [] + completion_items: list[str] = [] if isinstance(arg_choices, ChoicesCallable): if not arg_choices.is_completer: choices_func = arg_choices.choices_provider @@ -771,14 +745,13 @@ def _complete_arg( # The default ArgparseCompleter class for a cmd2 app -DEFAULT_AP_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter +DEFAULT_AP_COMPLETER: type[ArgparseCompleter] = ArgparseCompleter -def set_default_ap_completer_type(completer_type: Type[ArgparseCompleter]) -> None: - """ - Set the default ArgparseCompleter class for a cmd2 app. +def set_default_ap_completer_type(completer_type: type[ArgparseCompleter]) -> None: + """Set the default ArgparseCompleter class for a cmd2 app. :param completer_type: Type that is a subclass of ArgparseCompleter. """ - global DEFAULT_AP_COMPLETER + global DEFAULT_AP_COMPLETER # noqa: PLW0603 DEFAULT_AP_COMPLETER = completer_type diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 88676e687..675995377 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -1,6 +1,4 @@ -# coding=utf-8 -""" -This module adds capabilities to argparse by patching a few of its functions. +"""This module adds capabilities to argparse by patching a few of its functions. It also defines a parser class called Cmd2ArgumentParser which improves error and help output over normal argparse. All cmd2 code uses this parser and it is recommended that developers of cmd2-based apps either use it or write their own @@ -230,6 +228,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) ZERO_OR_MORE, ArgumentError, ) +from collections.abc import Callable, Iterable, Sequence from gettext import ( gettext, ) @@ -237,17 +236,9 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) IO, TYPE_CHECKING, Any, - Callable, - Dict, - Iterable, - List, NoReturn, Optional, Protocol, - Sequence, - Set, - Tuple, - Type, Union, cast, runtime_checkable, @@ -264,8 +255,8 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) ) -def generate_range_error(range_min: int, range_max: Union[int, float]) -> str: - """Generate an error message when the the number of arguments provided is not within the expected range""" +def generate_range_error(range_min: int, range_max: float) -> str: + """Generate an error message when the the number of arguments provided is not within the expected range.""" err_str = "expected " if range_max == constants.INFINITY: @@ -283,19 +274,17 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str: return err_str -class CompletionItem(str): - """ - Completion item with descriptive text attached +class CompletionItem(str): # noqa: SLOT000 + """Completion item with descriptive text attached. See header of this file for more information """ def __new__(cls, value: object, *args: Any, **kwargs: Any) -> 'CompletionItem': - return super(CompletionItem, cls).__new__(cls, value) + return super().__new__(cls, value) def __init__(self, value: object, description: str = '', *args: Any) -> None: - """ - CompletionItem Initializer + """CompletionItem Initializer. :param value: the value being tab completed :param description: description text to display @@ -310,7 +299,7 @@ def __init__(self, value: object, description: str = '', *args: Any) -> None: @property def orig_value(self) -> Any: - """Read-only property for _orig_value""" + """Read-only property for _orig_value.""" return self._orig_value @@ -321,20 +310,16 @@ def orig_value(self) -> Any: @runtime_checkable class ChoicesProviderFuncBase(Protocol): - """ - Function that returns a list of choices in support of tab completion - """ + """Function that returns a list of choices in support of tab completion.""" - def __call__(self) -> List[str]: ... # pragma: no cover + def __call__(self) -> list[str]: ... # pragma: no cover @runtime_checkable class ChoicesProviderFuncWithTokens(Protocol): - """ - Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments. - """ + """Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.""" - def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: ... # pragma: no cover + def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: ... # pragma: no cover ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens] @@ -342,9 +327,7 @@ def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: ... @runtime_checkable class CompleterFuncBase(Protocol): - """ - Function to support tab completion with the provided state of the user prompt - """ + """Function to support tab completion with the provided state of the user prompt.""" def __call__( self, @@ -352,13 +335,12 @@ def __call__( line: str, begidx: int, endidx: int, - ) -> List[str]: ... # pragma: no cover + ) -> list[str]: ... # pragma: no cover @runtime_checkable class CompleterFuncWithTokens(Protocol): - """ - Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior + """Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior arguments. """ @@ -369,16 +351,15 @@ def __call__( begidx: int, endidx: int, *, - arg_tokens: Dict[str, List[str]] = {}, - ) -> List[str]: ... # pragma: no cover + arg_tokens: dict[str, list[str]] = {}, + ) -> list[str]: ... # pragma: no cover CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] class ChoicesCallable: - """ - Enables using a callable as the choices provider for an argparse argument. + """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. """ @@ -387,11 +368,10 @@ def __init__( is_completer: bool, to_call: Union[CompleterFunc, ChoicesProviderFunc], ) -> None: - """ - Initializer + """Initializer :param is_completer: True if to_call is a tab completion routine which expects the args: text, line, begidx, endidx - :param to_call: the callable object that will be called to provide choices for the argument + :param to_call: the callable object that will be called to provide choices for the argument. """ self.is_completer = is_completer if is_completer: @@ -400,13 +380,12 @@ def __init__( raise ValueError( 'With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens' ) - else: - if not isinstance(to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)): # pragma: no cover - # runtime checking of Protocols do not currently check the parameters of a function. - raise ValueError( - 'With is_completer set to false, to_call must be either: ' - 'ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens' - ) + elif not isinstance(to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)): # pragma: no cover + # runtime checking of Protocols do not currently check the parameters of a function. + raise ValueError( + 'With is_completer set to false, to_call must be either: ' + 'ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens' + ) self.to_call = to_call @property @@ -446,8 +425,7 @@ def choices_provider(self) -> ChoicesProviderFunc: # Patch argparse.Action with accessors for choice_callable attribute ############################################################################################################ def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCallable]: - """ - Get the choices_callable attribute of an argparse Action. + """Get the choices_callable attribute of an argparse Action. This function is added by cmd2 as a method called ``get_choices_callable()`` to ``argparse.Action`` class. @@ -463,8 +441,7 @@ def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCalla def _action_set_choices_callable(self: argparse.Action, choices_callable: ChoicesCallable) -> None: - """ - Set the choices_callable attribute of an argparse Action. + """Set the choices_callable attribute of an argparse Action. This function is added by cmd2 as a method called ``_set_choices_callable()`` to ``argparse.Action`` class. @@ -478,7 +455,7 @@ def _action_set_choices_callable(self: argparse.Action, choices_callable: Choice if self.choices is not None: err_msg = "None of the following parameters can be used alongside a choices parameter:\nchoices_provider, completer" raise (TypeError(err_msg)) - elif self.nargs == 0: + if self.nargs == 0: err_msg = ( "None of the following parameters can be used on an action that takes no arguments:\nchoices_provider, completer" ) @@ -494,8 +471,7 @@ def _action_set_choices_provider( self: argparse.Action, choices_provider: ChoicesProviderFunc, ) -> None: - """ - Set choices_provider of an argparse Action. + """Set choices_provider of an argparse Action. This function is added by cmd2 as a method called ``set_choices_callable()`` to ``argparse.Action`` class. @@ -515,8 +491,7 @@ def _action_set_completer( self: argparse.Action, completer: CompleterFunc, ) -> None: - """ - Set completer of an argparse Action. + """Set completer of an argparse Action. This function is added by cmd2 as a method called ``set_completer()`` to ``argparse.Action`` class. @@ -536,8 +511,7 @@ def _action_set_completer( # Patch argparse.Action with accessors for descriptive_header attribute ############################################################################################################ def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]: - """ - Get the descriptive_header attribute of an argparse Action. + """Get the descriptive_header attribute of an argparse Action. This function is added by cmd2 as a method called ``get_descriptive_header()`` to ``argparse.Action`` class. @@ -553,8 +527,7 @@ def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]: def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Optional[str]) -> None: - """ - Set the descriptive_header attribute of an argparse Action. + """Set the descriptive_header attribute of an argparse Action. This function is added by cmd2 as a method called ``set_descriptive_header()`` to ``argparse.Action`` class. @@ -572,9 +545,8 @@ def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Op ############################################################################################################ # Patch argparse.Action with accessors for nargs_range attribute ############################################################################################################ -def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[int, float]]]: - """ - Get the nargs_range attribute of an argparse Action. +def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[int, float]]]: + """Get the nargs_range attribute of an argparse Action. This function is added by cmd2 as a method called ``get_nargs_range()`` to ``argparse.Action`` class. @@ -583,15 +555,14 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[ :param self: argparse Action being queried :return: The value of nargs_range or None if attribute does not exist """ - return cast(Optional[Tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None)) + return cast(Optional[tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None)) setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range) -def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[Tuple[int, Union[int, float]]]) -> None: - """ - Set the nargs_range attribute of an argparse Action. +def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[tuple[int, Union[int, float]]]) -> None: + """Set the nargs_range attribute of an argparse Action. This function is added by cmd2 as a method called ``set_nargs_range()`` to ``argparse.Action`` class. @@ -610,8 +581,7 @@ def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[Tuple[i # Patch argparse.Action with accessors for suppress_tab_hint attribute ############################################################################################################ def _action_get_suppress_tab_hint(self: argparse.Action) -> bool: - """ - Get the suppress_tab_hint attribute of an argparse Action. + """Get the suppress_tab_hint attribute of an argparse Action. This function is added by cmd2 as a method called ``get_suppress_tab_hint()`` to ``argparse.Action`` class. @@ -627,8 +597,7 @@ def _action_get_suppress_tab_hint(self: argparse.Action) -> bool: def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool) -> None: - """ - Set the suppress_tab_hint attribute of an argparse Action. + """Set the suppress_tab_hint attribute of an argparse Action. This function is added by cmd2 as a method called ``set_suppress_tab_hint()`` to ``argparse.Action`` class. @@ -647,13 +616,12 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool # Allow developers to add custom action attributes ############################################################################################################ -CUSTOM_ACTION_ATTRIBS: Set[str] = set() +CUSTOM_ACTION_ATTRIBS: set[str] = set() _CUSTOM_ATTRIB_PFX = '_attr_' -def register_argparse_argument_parameter(param_name: str, param_type: Optional[Type[Any]]) -> None: - """ - Registers a custom argparse argument parameter. +def register_argparse_argument_parameter(param_name: str, param_type: Optional[type[Any]]) -> None: + """Registers a custom argparse argument parameter. The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function. @@ -720,15 +688,14 @@ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None: def _add_argument_wrapper( self: argparse._ActionsContainer, *args: Any, - nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None, + nargs: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] = None, choices_provider: Optional[ChoicesProviderFunc] = None, completer: Optional[CompleterFunc] = None, suppress_tab_hint: bool = False, descriptive_header: Optional[str] = None, **kwargs: Any, ) -> argparse.Action: - """ - Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2 + """Wrapper around _ActionsContainer.add_argument() which supports more settings used by cmd2. # Args from original function :param self: instance of the _ActionsContainer being added to @@ -771,7 +738,7 @@ def _add_argument_wrapper( nargs_range = None if nargs is not None: - nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] + nargs_adjusted: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] # Check if nargs was given as a range if isinstance(nargs, tuple): # Handle 1-item tuple by setting max to INFINITY @@ -821,10 +788,7 @@ def _add_argument_wrapper( kwargs['nargs'] = nargs_adjusted # Extract registered custom keyword arguments - custom_attribs: Dict[str, Any] = {} - for keyword, value in kwargs.items(): - if keyword in CUSTOM_ACTION_ATTRIBS: - custom_attribs[keyword] = value + custom_attribs = {keyword: value for keyword, value in kwargs.items() if keyword in CUSTOM_ACTION_ATTRIBS} for keyword in custom_attribs: del kwargs[keyword] @@ -919,9 +883,8 @@ def _match_argument_wrapper(self: argparse.ArgumentParser, action: argparse.Acti ATTR_AP_COMPLETER_TYPE = 'ap_completer_type' -def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Optional[Type['ArgparseCompleter']]: - """ - Get the ap_completer_type attribute of an argparse ArgumentParser. +def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Optional[type['ArgparseCompleter']]: # noqa: N802 + """Get the ap_completer_type attribute of an argparse ArgumentParser. This function is added by cmd2 as a method called ``get_ap_completer_type()`` to ``argparse.ArgumentParser`` class. @@ -930,15 +893,14 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Opti :param self: ArgumentParser being queried :return: An ArgparseCompleter-based class or None if attribute does not exist """ - return cast(Optional[Type['ArgparseCompleter']], getattr(self, ATTR_AP_COMPLETER_TYPE, None)) + return cast(Optional[type['ArgparseCompleter']], getattr(self, ATTR_AP_COMPLETER_TYPE, None)) setattr(argparse.ArgumentParser, 'get_ap_completer_type', _ArgumentParser_get_ap_completer_type) -def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_completer_type: Type['ArgparseCompleter']) -> None: - """ - Set the ap_completer_type attribute of an argparse ArgumentParser. +def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_completer_type: type['ArgparseCompleter']) -> None: # noqa: N802 + """Set the ap_completer_type attribute of an argparse ArgumentParser. This function is added by cmd2 as a method called ``set_ap_completer_type()`` to ``argparse.ArgumentParser`` class. @@ -956,9 +918,8 @@ def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_comp ############################################################################################################ # Patch ArgumentParser._check_value to support CompletionItems as choices ############################################################################################################ -def _ArgumentParser_check_value(self: argparse.ArgumentParser, action: argparse.Action, value: Any) -> None: - """ - Custom override of ArgumentParser._check_value that supports CompletionItems as choices. +def _ArgumentParser_check_value(self: argparse.ArgumentParser, action: argparse.Action, value: Any) -> None: # noqa: N802 + """Custom override of ArgumentParser._check_value that supports CompletionItems as choices. When evaluating choices, input is compared to CompletionItem.orig_value instead of the CompletionItem instance. @@ -989,9 +950,8 @@ def _ArgumentParser_check_value(self: argparse.ArgumentParser, action: argparse. ############################################################################################################ -def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: # type: ignore - """ - Removes a sub-parser from a sub-parsers group. Used to remove subcommands from a parser. +def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None: # type: ignore[type-arg] # noqa: N802 + """Removes a sub-parser from a sub-parsers group. Used to remove subcommands from a parser. This function is added by cmd2 as a method called ``remove_parser()`` to ``argparse._SubParsersAction`` class. @@ -1029,7 +989,7 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): - """Custom help formatter to configure ordering of help text""" + """Custom help formatter to configure ordering of help text.""" def _format_usage( self, @@ -1043,15 +1003,15 @@ def _format_usage( # if usage is specified, use that if usage is not None: - usage %= dict(prog=self._prog) + usage %= {"prog": self._prog} # if no optionals or positionals are available, usage is just prog elif not actions: - usage = '%(prog)s' % dict(prog=self._prog) + usage = '%(prog)s' % {"prog": self._prog} # if optionals and positionals are available, calculate usage else: - prog = '%(prog)s' % dict(prog=self._prog) + prog = '%(prog)s' % {"prog": self._prog} # split optionals from positionals optionals = [] @@ -1069,8 +1029,8 @@ def _format_usage( # End cmd2 customization # build full usage string - format = self._format_actions_usage - action_usage = format(required_options + optionals + positionals, groups) # type: ignore[arg-type] + format_actions = self._format_actions_usage + action_usage = format_actions(required_options + optionals + positionals, groups) # type: ignore[arg-type] usage = ' '.join([s for s in [prog, action_usage] if s]) # wrap the usage parts if it's too long @@ -1080,26 +1040,20 @@ def _format_usage( # break usage into wrappable parts part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' - req_usage = format(required_options, groups) # type: ignore[arg-type] - opt_usage = format(optionals, groups) # type: ignore[arg-type] - pos_usage = format(positionals, groups) # type: ignore[arg-type] + req_usage = format_actions(required_options, groups) # type: ignore[arg-type] + opt_usage = format_actions(optionals, groups) # type: ignore[arg-type] + pos_usage = format_actions(positionals, groups) # type: ignore[arg-type] req_parts = re.findall(part_regexp, req_usage) opt_parts = re.findall(part_regexp, opt_usage) pos_parts = re.findall(part_regexp, pos_usage) - assert ' '.join(req_parts) == req_usage - assert ' '.join(opt_parts) == opt_usage - assert ' '.join(pos_parts) == pos_usage # End cmd2 customization # helper for wrapping lines - def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> List[str]: - lines: List[str] = [] - line: List[str] = [] - if prefix is not None: - line_len = len(prefix) - 1 - else: - line_len = len(indent) - 1 + def get_lines(parts: list[str], indent: str, prefix: Optional[str] = None) -> list[str]: + lines: list[str] = [] + line: list[str] = [] + line_len = len(prefix) - 1 if prefix is not None else len(indent) - 1 for part in parts: if line_len + 1 + len(part) > text_width and line: lines.append(indent + ' '.join(line)) @@ -1118,14 +1072,14 @@ def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> Li indent = ' ' * (len(prefix) + len(prog) + 1) # Begin cmd2 customization if req_parts: - lines = get_lines([prog] + req_parts, indent, prefix) + lines = get_lines([prog, *req_parts], indent, prefix) lines.extend(get_lines(opt_parts, indent)) lines.extend(get_lines(pos_parts, indent)) elif opt_parts: - lines = get_lines([prog] + opt_parts, indent, prefix) + lines = get_lines([prog, *opt_parts], indent, prefix) lines.extend(get_lines(pos_parts, indent)) elif pos_parts: - lines = get_lines([prog] + pos_parts, indent, prefix) + lines = get_lines([prog, *pos_parts], indent, prefix) else: lines = [prog] # End cmd2 customization @@ -1142,7 +1096,7 @@ def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> Li lines.extend(get_lines(opt_parts, indent)) lines.extend(get_lines(pos_parts, indent)) # End cmd2 customization - lines = [prog] + lines + lines = [prog, *lines] # join lines into usage usage = '\n'.join(lines) @@ -1156,31 +1110,29 @@ def _format_action_invocation(self, action: argparse.Action) -> str: (metavar,) = self._metavar_formatter(action, default)(1) return metavar - else: - parts: List[str] = [] + parts: list[str] = [] - # if the Optional doesn't take a value, format is: - # -s, --long - if action.nargs == 0: - parts.extend(action.option_strings) - return ', '.join(parts) + # if the Optional doesn't take a value, format is: + # -s, --long + if action.nargs == 0: + parts.extend(action.option_strings) + return ', '.join(parts) - # Begin cmd2 customization (less verbose) - # if the Optional takes a value, format is: - # -s, --long ARGS - else: - default = self._get_default_metavar_for_optional(action) - args_string = self._format_args(action, default) + # Begin cmd2 customization (less verbose) + # if the Optional takes a value, format is: + # -s, --long ARGS + default = self._get_default_metavar_for_optional(action) + args_string = self._format_args(action, default) - return ', '.join(action.option_strings) + ' ' + args_string - # End cmd2 customization + return ', '.join(action.option_strings) + ' ' + args_string + # End cmd2 customization def _determine_metavar( self, action: argparse.Action, - default_metavar: Union[str, Tuple[str, ...]], - ) -> Union[str, Tuple[str, ...]]: - """Custom method to determine what to use as the metavar value of an action""" + default_metavar: Union[str, tuple[str, ...]], + ) -> Union[str, tuple[str, ...]]: + """Custom method to determine what to use as the metavar value of an action.""" if action.metavar is not None: result = action.metavar elif action.choices is not None: @@ -1195,48 +1147,44 @@ def _determine_metavar( def _metavar_formatter( self, action: argparse.Action, - default_metavar: Union[str, Tuple[str, ...]], - ) -> Callable[[int], Tuple[str, ...]]: + default_metavar: Union[str, tuple[str, ...]], + ) -> Callable[[int], tuple[str, ...]]: metavar = self._determine_metavar(action, default_metavar) - def format(tuple_size: int) -> Tuple[str, ...]: + def format_tuple(tuple_size: int) -> tuple[str, ...]: if isinstance(metavar, tuple): return metavar - else: - return (metavar,) * tuple_size + return (metavar,) * tuple_size - return format + return format_tuple - def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tuple[str, ...]]) -> str: - """Customized to handle ranged nargs and make other output less verbose""" + def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str: + """Customized to handle ranged nargs and make other output less verbose.""" metavar = self._determine_metavar(action, default_metavar) metavar_formatter = self._metavar_formatter(action, default_metavar) # Handle nargs specified as a range nargs_range = action.get_nargs_range() # type: ignore[attr-defined] if nargs_range is not None: - if nargs_range[1] == constants.INFINITY: - range_str = f'{nargs_range[0]}+' - else: - range_str = f'{nargs_range[0]}..{nargs_range[1]}' + range_str = f'{nargs_range[0]}+' if nargs_range[1] == constants.INFINITY else f'{nargs_range[0]}..{nargs_range[1]}' return '{}{{{}}}'.format('%s' % metavar_formatter(1), range_str) # Make this output less verbose. Do not customize the output when metavar is a # tuple of strings. Allow argparse's formatter to handle that instead. - elif isinstance(metavar, str): + if isinstance(metavar, str): if action.nargs == ZERO_OR_MORE: return '[%s [...]]' % metavar_formatter(1) - elif action.nargs == ONE_OR_MORE: + if action.nargs == ONE_OR_MORE: return '%s [...]' % metavar_formatter(1) - elif isinstance(action.nargs, int) and action.nargs > 1: + if isinstance(action.nargs, int) and action.nargs > 1: return '{}{{{}}}'.format('%s' % metavar_formatter(1), action.nargs) return super()._format_args(action, default_metavar) # type: ignore[arg-type] class Cmd2ArgumentParser(argparse.ArgumentParser): - """Custom ArgumentParser class that improves error and help output""" + """Custom ArgumentParser class that improves error and help output.""" def __init__( self, @@ -1245,7 +1193,7 @@ def __init__( description: Optional[str] = None, epilog: Optional[str] = None, parents: Sequence[argparse.ArgumentParser] = (), - formatter_class: Type[argparse.HelpFormatter] = Cmd2HelpFormatter, + formatter_class: type[argparse.HelpFormatter] = Cmd2HelpFormatter, prefix_chars: str = '-', fromfile_prefix_chars: Optional[str] = None, argument_default: Optional[str] = None, @@ -1256,18 +1204,17 @@ def __init__( suggest_on_error: bool = False, color: bool = False, *, - ap_completer_type: Optional[Type['ArgparseCompleter']] = None, + ap_completer_type: Optional[type['ArgparseCompleter']] = None, ) -> None: - """ - # Custom parameter added by cmd2 + """# Custom parameter added by cmd2. :param ap_completer_type: optional parameter which specifies a subclass of ArgparseCompleter for custom tab completion behavior on this parser. If this is None or not present, then cmd2 will use argparse_completer.DEFAULT_AP_COMPLETER when tab completing this parser's arguments """ - if sys.version_info[1] >= 14: + if sys.version_info >= (3, 14): # Python >= 3.14 so pass new arguments to parent argparse.ArgumentParser class - super(Cmd2ArgumentParser, self).__init__( + super().__init__( prog=prog, usage=usage, description=description, @@ -1286,7 +1233,7 @@ def __init__( ) else: # Python < 3.14, so don't pass new arguments to parent argparse.ArgumentParser class - super(Cmd2ArgumentParser, self).__init__( + super().__init__( prog=prog, usage=usage, description=description, @@ -1304,9 +1251,8 @@ def __init__( self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined] - def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore - """ - Custom override. Sets a default title if one was not given. + def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore[type-arg] + """Custom override. Sets a default title if one was not given. :param kwargs: additional keyword arguments :return: argparse Subparser Action @@ -1317,7 +1263,7 @@ def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: return super().add_subparsers(**kwargs) def error(self, message: str) -> NoReturn: - """Custom override that applies custom formatting to the error message""" + """Custom override that applies custom formatting to the error message.""" lines = message.split('\n') linum = 0 formatted_message = '' @@ -1333,7 +1279,7 @@ def error(self, message: str) -> NoReturn: self.exit(2, f'{formatted_message}\n\n') def format_help(self) -> str: - """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters""" + """Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters.""" formatter = self._get_formatter() # usage @@ -1395,8 +1341,7 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: class Cmd2AttributeWrapper: - """ - Wraps a cmd2-specific attribute added to an argparse Namespace. + """Wraps a cmd2-specific attribute added to an argparse Namespace. This makes it easy to know which attributes in a Namespace are arguments from a parser and which were added by cmd2. """ @@ -1405,22 +1350,21 @@ def __init__(self, attribute: Any) -> None: self.__attribute = attribute def get(self) -> Any: - """Get the value of the attribute""" + """Get the value of the attribute.""" return self.__attribute def set(self, new_val: Any) -> None: - """Set the value of the attribute""" + """Set the value of the attribute.""" self.__attribute = new_val # The default ArgumentParser class for a cmd2 app -DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser +DEFAULT_ARGUMENT_PARSER: type[argparse.ArgumentParser] = Cmd2ArgumentParser -def set_default_argument_parser_type(parser_type: Type[argparse.ArgumentParser]) -> None: - """ - Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if +def set_default_argument_parser_type(parser_type: type[argparse.ArgumentParser]) -> None: + """Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if you want to override the parser for cmd2's built-in commands. See examples/override_parser.py. """ - global DEFAULT_ARGUMENT_PARSER + global DEFAULT_ARGUMENT_PARSER # noqa: PLW0603 DEFAULT_ARGUMENT_PARSER = parser_type diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 454e3484c..9b6cfc775 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -1,7 +1,4 @@ -# coding=utf-8 -""" -This module provides basic ability to copy from and paste to the clipboard/pastebuffer. -""" +"""This module provides basic ability to copy from and paste to the clipboard/pastebuffer.""" import typing @@ -13,8 +10,7 @@ def get_paste_buffer() -> str: :return: contents of the clipboard """ - pb_str = typing.cast(str, pyperclip.paste()) - return pb_str + return typing.cast(str, pyperclip.paste()) def write_to_paste_buffer(txt: str) -> None: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 7e34b7dcc..3d760436d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1,4 +1,3 @@ -# coding=utf-8 """Variant on standard library's cmd with extra features. To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you @@ -48,8 +47,10 @@ OrderedDict, namedtuple, ) +from collections.abc import Callable, Iterable, Mapping from contextlib import ( redirect_stdout, + suppress, ) from types import ( FrameType, @@ -59,16 +60,8 @@ IO, TYPE_CHECKING, Any, - Callable, - Dict, - Iterable, - List, - Mapping, Optional, - Set, TextIO, - Tuple, - Type, TypeVar, Union, cast, @@ -187,7 +180,7 @@ class _SavedReadlineSettings: - """readline settings that are backed up when switching between readline environments""" + """readline settings that are backed up when switching between readline environments.""" def __init__(self) -> None: self.completer = None @@ -196,18 +189,18 @@ def __init__(self) -> None: class _SavedCmd2Env: - """cmd2 environment settings that are backed up when entering an interactive Python shell""" + """cmd2 environment settings that are backed up when entering an interactive Python shell.""" def __init__(self) -> None: self.readline_settings = _SavedReadlineSettings() self.readline_module: Optional[ModuleType] = None - self.history: List[str] = [] + self.history: list[str] = [] self.sys_stdout: Optional[TextIO] = None self.sys_stdin: Optional[TextIO] = None # Contains data about a disabled command which is used to restore its original functions when the command is enabled -DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) +DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024 if TYPE_CHECKING: # pragma: no cover @@ -219,8 +212,7 @@ def __init__(self) -> None: class _CommandParsers: - """ - Create and store all command method argument parsers for a given Cmd instance. + """Create and store all command method argument parsers for a given Cmd instance. Parser creation and retrieval are accomplished through the get() method. """ @@ -230,7 +222,7 @@ def __init__(self, cmd: 'Cmd') -> None: # Keyed by the fully qualified method names. This is more reliable than # the methods themselves, since wrapping a method will change its address. - self._parsers: Dict[str, argparse.ArgumentParser] = {} + self._parsers: dict[str, argparse.ArgumentParser] = {} @staticmethod def _fully_qualified_name(command_method: CommandFunc) -> str: @@ -241,8 +233,7 @@ def _fully_qualified_name(command_method: CommandFunc) -> str: return "" def __contains__(self, command_method: CommandFunc) -> bool: - """ - Return whether a given method's parser is in self. + """Return whether a given method's parser is in self. If the parser does not yet exist, it will be created if applicable. This is basically for checking if a method is argarse-based. @@ -251,8 +242,7 @@ def __contains__(self, command_method: CommandFunc) -> bool: return bool(parser) def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]: - """ - Return a given method's parser or None if the method is not argparse-based. + """Return a given method's parser or None if the method is not argparse-based. If the parser does not yet exist, it will be created. """ @@ -325,11 +315,11 @@ def __init__( include_py: bool = False, include_ipy: bool = False, allow_cli_args: bool = True, - transcript_files: Optional[List[str]] = None, + 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, + 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, allow_clipboard: bool = True, @@ -419,12 +409,12 @@ def __init__( self.max_completion_items = 50 # A dictionary mapping settable names to their Settable instance - self._settables: Dict[str, Settable] = dict() + self._settables: dict[str, Settable] = {} self._always_prefix_settables: bool = False # CommandSet containers - self._installed_command_sets: Set[CommandSet] = set() - self._cmd_to_command_sets: Dict[str, CommandSet] = {} + self._installed_command_sets: set[CommandSet] = set() + self._cmd_to_command_sets: dict[str, CommandSet] = {} self.build_settables() @@ -446,16 +436,16 @@ def __init__( self.exclude_from_history = ['eof', 'history'] # Dictionary of macro names and their values - self.macros: Dict[str, Macro] = dict() + self.macros: dict[str, Macro] = {} # Keeps track of typed command history in the Python shell - self._py_history: List[str] = [] + self._py_history: list[str] = [] # The name by which Python environments refer to the PyBridge to call app commands self.py_bridge_name = 'app' # Defines app-specific variables/functions available in Python shells and pyscripts - self.py_locals: Dict[str, Any] = dict() + self.py_locals: dict[str, Any] = {} # True if running inside a Python shell or pyscript, False otherwise self._in_py = False @@ -468,7 +458,7 @@ def __init__( self.last_result: Any = None # Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command - self._script_dir: List[str] = [] + self._script_dir: list[str] = [] # Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt self.sigint_protection = utils.ContextFlag() @@ -499,7 +489,7 @@ def __init__( self.broken_pipe_warning = '' # Commands that will run at the beginning of the command loop - self._startup_commands: List[str] = [] + self._startup_commands: list[str] = [] # If a startup script is provided and exists, then execute it in the startup commands if startup_script: @@ -511,7 +501,7 @@ def __init__( self._startup_commands.append(script_cmd) # Transcript files to run instead of interactive command loop - self._transcript_files: Optional[List[str]] = None + self._transcript_files: Optional[list[str]] = None # Check for command line args if allow_cli_args: @@ -554,7 +544,7 @@ def __init__( # Commands that have been disabled from use. This is to support commands that are only available # during specific states of the application. This dictionary's keys are the command names and its # values are DisabledCommand objects. - self.disabled_commands: Dict[str, DisabledCommand] = dict() + self.disabled_commands: dict[str, DisabledCommand] = {} # If any command has been categorized, then all other commands that haven't been categorized # will display under this section in the help output. @@ -592,7 +582,7 @@ def __init__( self.formatted_completions = '' # Used by complete() for readline tab completion - self.completion_matches: List[str] = [] + self.completion_matches: list[str] = [] # Use this list if you need to display tab completion suggestions that are different than the actual text # of the matches. For instance, if you are completing strings that contain a common delimiter and you only @@ -600,7 +590,7 @@ def __init__( # still must be returned from your completer function. For an example, look at path_complete() which # uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates # this list. These are ignored if self.formatted_completions is populated. - self.display_matches: List[str] = [] + self.display_matches: list[str] = [] # Used by functions like path_complete() and delimiter_complete() to properly # quote matches that are completed in a delimited fashion @@ -642,14 +632,13 @@ def __init__( # the current command being executed self.current_command: Optional[Statement] = None - def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]: - """ - Find all CommandSets that match the provided CommandSet type. + def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]: + """Find all CommandSets that match the provided CommandSet type. By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that are sub-classes of the provided type :param commandset_type: CommandSet sub-class type to search for :param subclass_match: If True, return all sub-classes of provided type, otherwise only search for exact match - :return: Matching CommandSets + :return: Matching CommandSets. """ return [ cmdset @@ -658,10 +647,9 @@ def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: ] def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]: - """ - Finds the CommandSet that registered the command name + """Finds the CommandSet that registered the command name :param command_name: command name to search - :return: CommandSet that provided the command + :return: CommandSet that provided the command. """ return self._cmd_to_command_sets.get(command_name) @@ -671,7 +659,7 @@ def _autoload_commands(self) -> None: all_commandset_defs = CommandSet.__subclasses__() existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets] - def load_commandset_by_type(commandset_types: List[Type[CommandSet]]) -> None: + def load_commandset_by_type(commandset_types: list[type[CommandSet]]) -> None: for cmdset_type in commandset_types: # check if the type has sub-classes. We will only auto-load leaf class types. subclasses = cmdset_type.__subclasses__() @@ -690,8 +678,7 @@ def load_commandset_by_type(commandset_types: List[Type[CommandSet]]) -> None: load_commandset_by_type(all_commandset_defs) def register_command_set(self, cmdset: CommandSet) -> None: - """ - Installs a CommandSet, loading all commands defined in the CommandSet + """Installs a CommandSet, loading all commands defined in the CommandSet. :param cmdset: CommandSet to load """ @@ -703,19 +690,19 @@ def register_command_set(self, cmdset: CommandSet) -> None: if self.always_prefix_settables: if not cmdset.settable_prefix.strip(): raise CommandSetRegistrationError('CommandSet settable prefix must not be empty') - for key in cmdset.settables.keys(): + for key in cmdset.settables: prefixed_name = f'{cmdset.settable_prefix}.{key}' if prefixed_name in all_settables: raise CommandSetRegistrationError(f'Duplicate settable: {key}') else: - for key in cmdset.settables.keys(): + for key in cmdset.settables: if key in all_settables: raise CommandSetRegistrationError(f'Duplicate settable {key} is already registered') cmdset.on_register(self) methods = cast( - List[Tuple[str, Callable[..., Any]]], + list[tuple[str, Callable[..., Any]]], inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] @@ -790,8 +777,7 @@ def _build_parser( return parser def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None: - """ - Install a new command function into the CLI. + """Install a new command function into the CLI. :param command_func_name: name of command function to add This points to the command method and may differ from the method's @@ -800,7 +786,6 @@ def _install_command_function(self, command_func_name: str, command_method: Comm :param context: optional info to provide in error message. (e.g. class this function belongs to) :raises CommandSetRegistrationError: if the command function fails to install """ - # command_func_name must begin with COMMAND_FUNC_PREFIX to be identified as a command by cmd2. if not command_func_name.startswith(COMMAND_FUNC_PREFIX): raise CommandSetRegistrationError(f"{command_func_name} does not begin with '{COMMAND_FUNC_PREFIX}'") @@ -847,8 +832,7 @@ def _install_help_function(self, cmd_name: str, cmd_help: Callable[..., None]) - setattr(self, help_func_name, cmd_help) def unregister_command_set(self, cmdset: CommandSet) -> None: - """ - Uninstalls a CommandSet and unloads all associated commands + """Uninstalls a CommandSet and unloads all associated commands. :param cmdset: CommandSet to uninstall """ @@ -857,7 +841,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: cmdset.on_unregister() self._unregister_subcommands(cmdset) - methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers( + methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -903,7 +887,7 @@ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None: check_parser_uninstallable(subparser) break - methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers( + methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -919,8 +903,7 @@ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None: check_parser_uninstallable(command_parser) def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: - """ - Register subcommands with their base command + """Register subcommands with their base command. :param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands """ @@ -944,7 +927,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True) if not subcommand_valid: - raise CommandSetRegistrationError(f'Subcommand {str(subcommand_name)} is not valid: {errmsg}') + raise CommandSetRegistrationError(f'Subcommand {subcommand_name!s} is not valid: {errmsg}') command_tokens = full_command_name.split() command_name = command_tokens[0] @@ -957,16 +940,14 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: command_func = self.cmd_func(command_name) if command_func is None: - raise CommandSetRegistrationError( - f"Could not find command '{command_name}' needed by subcommand: {str(method)}" - ) + raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}") command_parser = self._command_parsers.get(command_func) if command_parser is None: raise CommandSetRegistrationError( - f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}" + f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}" ) - def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser: + def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: if not subcmd_names: return action cur_subcmd = subcmd_names.pop(0) @@ -1028,8 +1009,7 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> break def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: - """ - Unregister subcommands from their base command + """Unregister subcommands from their base command. :param cmdset: CommandSet containing subcommands """ @@ -1059,15 +1039,13 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: 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( - f"Could not find command '{command_name}' needed by subcommand: {str(method)}" - ) + raise CommandSetRegistrationError(f"Could not find command '{command_name}' needed by subcommand: {method!s}") command_parser = self._command_parsers.get(command_func) 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( - f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}" + f"Could not find argparser for command '{command_name}' needed by subcommand: {method!s}" ) for action in command_parser._actions: @@ -1077,8 +1055,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: @property def always_prefix_settables(self) -> bool: - """ - Flags whether CommandSet settable values should always be prefixed + """Flags whether CommandSet settable values should always be prefixed. :return: True if CommandSet settable values will always be prefixed. False if not. """ @@ -1086,8 +1063,7 @@ def always_prefix_settables(self) -> bool: @always_prefix_settables.setter def always_prefix_settables(self, new_value: bool) -> None: - """ - Set whether CommandSet settable values should always be prefixed. + """Set whether CommandSet settable values should always be prefixed. :param new_value: True if CommandSet settable values should always be prefixed. False if not. :raises ValueError: If a registered CommandSet does not have a defined prefix @@ -1103,8 +1079,7 @@ def always_prefix_settables(self, new_value: bool) -> None: @property def settables(self) -> Mapping[str, Settable]: - """ - Get all available user-settable attributes. This includes settables defined in installed CommandSets + """Get all available user-settable attributes. This includes settables defined in installed CommandSets. :return: Mapping from attribute-name to Settable of all user-settable attributes from """ @@ -1119,19 +1094,17 @@ def settables(self) -> Mapping[str, Settable]: return all_settables def add_settable(self, settable: Settable) -> None: - """ - Add a settable parameter to ``self.settables`` + """Add a settable parameter to ``self.settables``. :param settable: Settable object being added """ if not self.always_prefix_settables: - if settable.name in self.settables.keys() and settable.name not in self._settables.keys(): + if settable.name in self.settables and settable.name not in self._settables: raise KeyError(f'Duplicate settable: {settable.name}') self._settables[settable.name] = settable def remove_settable(self, name: str) -> None: - """ - Convenience method for removing a settable parameter from ``self.settables`` + """Convenience method for removing a settable parameter from ``self.settables``. :param name: name of the settable being removed :raises KeyError: if the Settable matches this name @@ -1142,14 +1115,14 @@ def remove_settable(self, name: str) -> None: raise KeyError(name + " is not a settable parameter") def build_settables(self) -> None: - """Create the dictionary of user-settable parameters""" + """Create the dictionary of user-settable parameters.""" - def get_allow_style_choices(cli_self: Cmd) -> List[str]: - """Used to tab complete allow_style values""" + def get_allow_style_choices(cli_self: Cmd) -> list[str]: + """Used to tab complete allow_style values.""" return [val.name.lower() for val in ansi.AllowStyle] def allow_style_type(value: str) -> ansi.AllowStyle: - """Converts a string value into an ansi.AllowStyle""" + """Converts a string value into an ansi.AllowStyle.""" try: return ansi.AllowStyle[value.upper()] except KeyError: @@ -1187,16 +1160,16 @@ def allow_style_type(value: str) -> ansi.AllowStyle: @property def allow_style(self) -> ansi.AllowStyle: - """Read-only property needed to support do_set when it reads allow_style""" + """Read-only property needed to support do_set when it reads allow_style.""" return ansi.allow_style @allow_style.setter def allow_style(self, new_val: ansi.AllowStyle) -> None: - """Setter property needed to support do_set when it updates allow_style""" + """Setter property needed to support do_set when it updates allow_style.""" ansi.allow_style = new_val def _completion_supported(self) -> bool: - """Return whether tab completion is supported""" + """Return whether tab completion is supported.""" return self.use_rawinput and bool(self.completekey) and rl_type != RlType.NONE @property @@ -1218,8 +1191,7 @@ def print_to( end: str = '\n', style: Optional[Callable[[str], str]] = None, ) -> None: - """ - Print message to a given file object. + """Print message to a given file object. :param dest: the file object being written to :param msg: object to print @@ -1239,7 +1211,7 @@ def print_to( sys.stderr.write(self.broken_pipe_warning) def poutput(self, msg: Any = '', *, end: str = '\n') -> None: - """Print message to self.stdout and appends a newline by default + """Print message to self.stdout and appends a newline by default. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1247,7 +1219,7 @@ def poutput(self, msg: Any = '', *, end: str = '\n') -> None: self.print_to(self.stdout, msg, end=end) def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None: - """Print message to sys.stderr + """Print message to sys.stderr. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1257,7 +1229,7 @@ def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> self.print_to(sys.stderr, msg, end=end, style=ansi.style_error if apply_style else None) def psuccess(self, msg: Any = '', *, end: str = '\n') -> None: - """Wraps poutput, but applies ansi.style_success by default + """Wraps poutput, but applies ansi.style_success by default. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1266,7 +1238,7 @@ def psuccess(self, msg: Any = '', *, end: str = '\n') -> None: self.poutput(msg, end=end) def pwarning(self, msg: Any = '', *, end: str = '\n') -> None: - """Wraps perror, but applies ansi.style_warning by default + """Wraps perror, but applies ansi.style_warning by default. :param msg: object to print :param end: string appended after the end of the message, default a newline @@ -1355,7 +1327,7 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: with self.sigint_protection: import subprocess - pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout) + pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE, stdout=self.stdout) # noqa: S602 pipe_proc.communicate(final_msg.encode('utf-8', 'replace')) except BrokenPipeError: # This occurs if a command's output is being piped to another process and that process closes before the @@ -1369,9 +1341,8 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: # ----- Methods related to tab completion ----- def _reset_completion_defaults(self) -> None: - """ - Resets tab completion settings - Needs to be called each time readline runs tab completion + """Resets tab completion settings + Needs to be called each time readline runs tab completion. """ self.allow_appended_space = True self.allow_closing_quote = True @@ -1387,7 +1358,7 @@ def _reset_completion_defaults(self) -> None: elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = self._display_matches_pyreadline - def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]: + def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]: """Used by tab completion functions to get all tokens through the one being completed. :param line: the current input line with leading whitespace removed @@ -1458,9 +1429,8 @@ def basic_complete( begidx: int, endidx: int, match_against: Iterable[str], - ) -> List[str]: - """ - Basic tab completion function that matches against a list of strings without considering line contents + ) -> list[str]: + """Basic tab completion function that matches against a list of strings without considering line contents or cursor position. The args required by this function are defined in the header of Python's cmd.py. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -1480,9 +1450,8 @@ def delimiter_complete( endidx: int, match_against: Iterable[str], delimiter: str, - ) -> List[str]: - """ - Performs tab completion against a list but each match is split on a delimiter and only + ) -> 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. This is useful if you match against strings that are hierarchical in nature and have a common delimiter. @@ -1546,10 +1515,10 @@ def flag_based_complete( line: str, begidx: int, endidx: int, - flag_dict: Dict[str, Union[Iterable[str], CompleterFunc]], + flag_dict: dict[str, Union[Iterable[str], CompleterFunc]], *, all_else: Union[None, Iterable[str], CompleterFunc] = None, - ) -> List[str]: + ) -> 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) @@ -1598,7 +1567,7 @@ def index_based_complete( index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]], *, all_else: Optional[Union[Iterable[str], CompleterFunc]] = None, - ) -> List[str]: + ) -> 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) @@ -1626,10 +1595,7 @@ def index_based_complete( # Check if token is at an index in the dictionary match_against: Optional[Union[Iterable[str], CompleterFunc]] - if index in index_dict: - match_against = index_dict[index] - else: - match_against = all_else + match_against = index_dict.get(index, all_else) # Perform tab completion using a Iterable if isinstance(match_against, Iterable): @@ -1643,8 +1609,8 @@ def index_based_complete( 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 + ) -> 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) :param line: the current input line with leading whitespace removed @@ -1657,7 +1623,7 @@ def path_complete( """ # Used to complete ~ and ~user strings - def complete_users() -> List[str]: + def complete_users() -> list[str]: users = [] # Windows lacks the pwd module so we can't get a list of users. @@ -1728,12 +1694,11 @@ def complete_users() -> List[str]: return complete_users() # Otherwise expand the user dir - else: - search_str = os.path.expanduser(search_str) + search_str = os.path.expanduser(search_str) - # Get what we need to restore the original tilde path later - orig_tilde_path = text[:sep_index] - expanded_tilde_path = os.path.expanduser(orig_tilde_path) + # Get what we need to restore the original tilde path later + orig_tilde_path = text[:sep_index] + expanded_tilde_path = os.path.expanduser(orig_tilde_path) # If the search text does not have a directory, then use the cwd elif not os.path.dirname(text): @@ -1772,10 +1737,7 @@ def complete_users() -> List[str]: # Remove cwd if it was added to match the text readline expects if cwd_added: - if cwd == os.path.sep: - to_replace = cwd - else: - to_replace = cwd + os.path.sep + to_replace = cwd if cwd == os.path.sep else cwd + os.path.sep matches = [cur_path.replace(to_replace, '', 1) for cur_path in matches] # Restore the tilde string if we expanded one to match the text readline expects @@ -1784,8 +1746,8 @@ def complete_users() -> List[str]: return matches - 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 + 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) :param line: the current input line with leading whitespace removed @@ -1804,15 +1766,14 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, return utils.get_exes_in_path(text) # 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: CompleterFunc) -> List[str]: + def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]: """Called by complete() as the first tab completion function for all commands It determines if it should tab complete for redirection (|, >, >>) or use the - completer function for the current command + completer function for the current command. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -1878,19 +1839,19 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com if do_shell_completion: return self.shell_cmd_complete(text, line, begidx, endidx) - elif do_path_completion: + if do_path_completion: return self.path_complete(text, line, begidx, endidx) # If there were redirection strings anywhere on the command line, then we # are no longer tab completing for the current command - elif has_redirection: + if has_redirection: return [] # Call the command's completer function return compfunc(text, line, begidx, endidx) @staticmethod - def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], int]: # pragma: no cover + def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover """Adds padding to the matches being displayed as tab completion suggestions. The default padding of readline/pyreadine is small and not visually appealing especially if matches have spaces. It appears very squished together. @@ -1912,9 +1873,9 @@ def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], i return [cur_match + padding for cur_match in matches_to_display], len(padding) def _display_matches_gnu_readline( - self, substitution: str, matches: List[str], longest_match_length: int + 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() + """Prints a match list using GNU readline's rl_display_match_list(). :param substitution: the substitution written to the command line :param matches: the tab completion matches to display @@ -1944,8 +1905,7 @@ def _display_matches_gnu_readline( for cur_match in matches_to_display: cur_length = ansi.style_aware_wcswidth(cur_match) - if cur_length > longest_match_length: - longest_match_length = cur_length + longest_match_length = max(longest_match_length, cur_length) else: matches_to_display = matches @@ -1960,7 +1920,7 @@ def _display_matches_gnu_readline( # rl_display_match_list() expects matches to be in argv format where # substitution is the first element, followed by the matches, and then a NULL. - strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) + strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) # Copy in the encoded strings and add a NULL to the end strings_array[0] = encoded_substitution @@ -1973,8 +1933,8 @@ def _display_matches_gnu_readline( # Redraw prompt and input line rl_force_redisplay() - def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no cover - """Prints a match list using pyreadline3's _display_completions() + def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover + """Prints a match list using pyreadline3's _display_completions(). :param matches: the tab completion matches to display """ @@ -1997,10 +1957,7 @@ def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no # Otherwise use pyreadline3's formatter else: # Check if we should show display_matches - if self.display_matches: - matches_to_display = self.display_matches - else: - matches_to_display = matches + matches_to_display = self.display_matches if self.display_matches else matches # Add padding for visual appeal matches_to_display, _ = self._pad_matches_to_display(matches_to_display) @@ -2009,15 +1966,14 @@ def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no orig_pyreadline_display(matches_to_display) @staticmethod - def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> Type[argparse_completer.ArgparseCompleter]: - """ - Determine what type of ArgparseCompleter to use on a given parser. If the parser does not have one + def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argparse_completer.ArgparseCompleter]: + """Determine what type of ArgparseCompleter to use on a given parser. If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER. :param parser: the parser to examine :return: type of ArgparseCompleter """ - Completer = Optional[Type[argparse_completer.ArgparseCompleter]] + Completer = Optional[type[argparse_completer.ArgparseCompleter]] # noqa: N806 completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined] if completer_type is None: @@ -2027,8 +1983,7 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> Type[argpar def _perform_completion( self, text: str, line: str, begidx: int, endidx: int, custom_settings: Optional[utils.CustomCompletionSettings] = None ) -> None: - """ - Helper function for complete() that performs the actual completion + """Helper function for complete() that performs the actual completion. :param text: the string prefix we are attempting to match (all matches must begin with it) :param line: the current input line with leading whitespace removed @@ -2106,12 +2061,11 @@ def _perform_completion( completer_func = self.completedefault # type: ignore[assignment] # Not a recognized macro or command + # Check if this command should be run as a shell command + elif self.default_to_shell and command in utils.get_exes_in_path(command): + completer_func = self.path_complete else: - # Check if this command should be run as a shell command - if self.default_to_shell and command in utils.get_exes_in_path(command): - completer_func = self.path_complete - else: - completer_func = self.completedefault # type: ignore[assignment] + completer_func = self.completedefault # type: ignore[assignment] # Otherwise we are completing the command token or performing custom completion else: @@ -2192,10 +2146,7 @@ def _perform_completion( if add_quote: # Figure out what kind of quote to add and save it as the unclosed_quote - if any('"' in match for match in self.completion_matches): - completion_token_quote = "'" - else: - completion_token_quote = '"' + completion_token_quote = "'" if any('"' in match for match in self.completion_matches) else '"' self.completion_matches = [completion_token_quote + match for match in self.completion_matches] @@ -2210,7 +2161,7 @@ def _perform_completion( def complete( # type: ignore[override] self, text: str, state: int, custom_settings: Optional[utils.CustomCompletionSettings] = None ) -> Optional[str]: - """Override of cmd's complete method which returns the next possible completion for 'text' + """Override of cmd's complete method which returns the next possible completion for 'text'. This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …, until it returns a non-string value. It should return the next possible completion starting with text. @@ -2304,7 +2255,7 @@ def complete( # type: ignore[override] ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n') rl_force_redisplay() return None - except Exception as ex: + except Exception as ex: # noqa: BLE001 # Insert a newline so the exception doesn't print in the middle of the command line being tab completed self.perror() self.pexcept(ex) @@ -2312,32 +2263,32 @@ def complete( # type: ignore[override] return None def in_script(self) -> bool: - """Return whether a text script is running""" + """Return whether a text script is running.""" return self._current_script_dir is not None def in_pyscript(self) -> bool: - """Return whether running inside a Python shell or pyscript""" + """Return whether running inside a Python shell or pyscript.""" return self._in_py @property - def aliases(self) -> Dict[str, str]: - """Read-only property to access the aliases stored in the StatementParser""" + def aliases(self) -> dict[str, str]: + """Read-only property to access the aliases stored in the StatementParser.""" return self.statement_parser.aliases - def get_names(self) -> List[str]: + def get_names(self) -> list[str]: """Return an alphabetized list of names comprising the attributes of the cmd2 class instance.""" return dir(self) - def get_all_commands(self) -> List[str]: - """Return a list of all commands""" + 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)) ] - def get_visible_commands(self) -> List[str]: - """Return a list of commands that have not been hidden or disabled""" + 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() @@ -2347,9 +2298,9 @@ def get_visible_commands(self) -> List[str]: # Table displayed when tab completing aliases _alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) - def _get_alias_completion_items(self) -> List[CompletionItem]: - """Return list of alias names and values as CompletionItems""" - results: List[CompletionItem] = [] + def _get_alias_completion_items(self) -> list[CompletionItem]: + """Return list of alias names and values as CompletionItems.""" + results: list[CompletionItem] = [] for cur_key in self.aliases: row_data = [self.aliases[cur_key]] @@ -2360,9 +2311,9 @@ def _get_alias_completion_items(self) -> List[CompletionItem]: # Table displayed when tab completing macros _macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) - def _get_macro_completion_items(self) -> List[CompletionItem]: - """Return list of macro names and values as CompletionItems""" - results: List[CompletionItem] = [] + def _get_macro_completion_items(self) -> list[CompletionItem]: + """Return list of macro names and values as CompletionItems.""" + results: list[CompletionItem] = [] for cur_key in self.macros: row_data = [self.macros[cur_key].value] @@ -2373,9 +2324,9 @@ def _get_macro_completion_items(self) -> List[CompletionItem]: # Table displayed when tab completing Settables _settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None) - def _get_settable_completion_items(self) -> List[CompletionItem]: - """Return list of Settable names, values, and descriptions as CompletionItems""" - results: List[CompletionItem] = [] + def _get_settable_completion_items(self) -> list[CompletionItem]: + """Return list of Settable names, values, and descriptions as CompletionItems.""" + results: list[CompletionItem] = [] for cur_key in self.settables: row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description] @@ -2383,15 +2334,15 @@ def _get_settable_completion_items(self) -> List[CompletionItem]: return results - def _get_commands_aliases_and_macros_for_completion(self) -> List[str]: - """Return a list of visible commands, aliases, and macros for tab completion""" + def _get_commands_aliases_and_macros_for_completion(self) -> list[str]: + """Return a list of visible commands, aliases, and macros for tab completion.""" visible_commands = set(self.get_visible_commands()) alias_names = set(self.aliases) macro_names = set(self.macros) return list(visible_commands | alias_names | macro_names) - def get_help_topics(self) -> List[str]: - """Return a list of help topics""" + 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() @@ -2424,8 +2375,7 @@ def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: self._raise_keyboard_interrupt() def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None: - """ - Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac. + """Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac. SIGHUP - received when terminal window is closed SIGTERM - received when this app has been requested to terminate @@ -2441,7 +2391,7 @@ def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> Non sys.exit(128 + signum) def _raise_keyboard_interrupt(self) -> None: - """Helper function to raise a KeyboardInterrupt""" + """Helper function to raise a KeyboardInterrupt.""" raise KeyboardInterrupt("Got a keyboard interrupt") def precmd(self, statement: Union[Statement, str]) -> Statement: @@ -2479,7 +2429,6 @@ def preloop(self) -> None: to run hooks before the command loop begins. See [Hooks](../features/hooks.md) for more information. """ - pass def postloop(self) -> None: """Hook method executed once when the [cmd2.Cmd.cmdloop][] method is about to return. @@ -2488,9 +2437,8 @@ def postloop(self) -> None: to run hooks after the command loop completes. See [Hooks](../features/hooks.md) for more information. """ - pass - def parseline(self, line: str) -> Tuple[str, str, str]: + def parseline(self, line: str) -> tuple[str, str, str]: """Parse the line into a command name and a string containing the arguments. NOTE: This is an override of a parent class method. It is only used by other parent class methods. @@ -2562,7 +2510,7 @@ def onecmd_plus_hooks( redir_saved_state = self._redirect_output(statement) - timestart = datetime.datetime.now() + timestart = datetime.datetime.now(tz=datetime.timezone.utc) # precommand hooks precmd_data = plugin.PrecommandData(statement) @@ -2588,7 +2536,7 @@ def onecmd_plus_hooks( stop = self.postcmd(stop, statement) if self.timing: - self.pfeedback(f'Elapsed: {datetime.datetime.now() - timestart}') + self.pfeedback(f'Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}') finally: # Get sigint protection while we restore stuff with self.sigint_protection: @@ -2605,43 +2553,43 @@ def onecmd_plus_hooks( self.perror(f"Invalid syntax: {ex}") except RedirectionError as ex: self.perror(ex) - except KeyboardInterrupt as ex: + except KeyboardInterrupt: if raise_keyboard_interrupt and not stop: - raise ex + raise except SystemExit as ex: if isinstance(ex.code, int): self.exit_code = ex.code stop = True except PassThroughException as ex: raise ex.wrapped_ex - except Exception as ex: + except Exception as ex: # noqa: BLE001 self.pexcept(ex) finally: try: stop = self._run_cmdfinalization_hooks(stop, statement) - except KeyboardInterrupt as ex: + except KeyboardInterrupt: if raise_keyboard_interrupt and not stop: - raise ex + raise except SystemExit as ex: if isinstance(ex.code, int): self.exit_code = ex.code stop = True except PassThroughException as ex: raise ex.wrapped_ex - except Exception as ex: + except Exception as ex: # noqa: BLE001 self.pexcept(ex) return stop def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool: - """Run the command finalization hooks""" + """Run the command finalization hooks.""" with self.sigint_protection: if not sys.platform.startswith('win') and self.stdin.isatty(): # 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 = subprocess.Popen(['stty', 'sane']) # noqa: S603, S607 proc.communicate() data = plugin.CommandFinalizationData(stop, statement) @@ -2653,13 +2601,12 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) def runcmds_plus_hooks( self, - cmds: Union[List[HistoryItem], List[str]], + cmds: Union[list[HistoryItem], list[str]], *, add_to_history: bool = True, stop_on_keyboard_interrupt: bool = False, ) -> bool: - """ - Used when commands are being run in an automated fashion like text scripts or history replays. + """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. :param cmds: commands to run @@ -2671,7 +2618,7 @@ def runcmds_plus_hooks( """ for line in cmds: if isinstance(line, HistoryItem): - line = line.raw + line = line.raw # noqa: PLW2901 if self.echo: self.poutput(f'{self.prompt}{line}') @@ -2706,7 +2653,7 @@ def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int """ def combine_rl_history(statement: Statement) -> None: - """Combine all lines of a multiline command into a single readline history entry""" + """Combine all lines of a multiline command into a single readline history entry.""" if orig_rl_history_length is None or not statement.multiline_command: return @@ -2773,15 +2720,13 @@ def combine_rl_history(statement: Statement) -> None: if not statement.command: raise EmptyStatement - else: - # If necessary, update history with completed multiline command. - combine_rl_history(statement) + # If necessary, update history with completed multiline command. + combine_rl_history(statement) return statement def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement: - """ - Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved + """Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved. :param line: the line being parsed :param orig_rl_history_length: Optional length of the readline history before the current command was typed. @@ -2806,7 +2751,7 @@ def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optiona orig_rl_history_length = None # Check if this command matches a macro and wasn't already processed to avoid an infinite loop - if statement.command in self.macros.keys() and statement.command not in used_macros: + if statement.command in self.macros and statement.command not in used_macros: used_macros.append(statement.command) resolve_result = self._resolve_macro(statement) if resolve_result is None: @@ -2834,13 +2779,12 @@ def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optiona return statement def _resolve_macro(self, statement: Statement) -> Optional[str]: - """ - Resolve a macro and return the resulting string + """Resolve a macro and return the resulting string. :param statement: the parsed statement from the command line :return: the resolved macro or None on error """ - if statement.command not in self.macros.keys(): + if statement.command not in self.macros: raise KeyError(f"{statement.command} is not a macro") macro = self.macros[statement.command] @@ -2881,7 +2825,6 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: :return: A bool telling if an error occurred and a utils.RedirectionSavedState object :raises RedirectionError: if an error occurs trying to pipe or redirect """ - import io import subprocess # Initialize the redirection saved state @@ -2901,13 +2844,13 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: read_fd, write_fd = os.pipe() # Open each side of the pipe - subproc_stdin = io.open(read_fd, 'r') - new_stdout: TextIO = cast(TextIO, io.open(write_fd, 'w')) + subproc_stdin = open(read_fd) + new_stdout: TextIO = cast(TextIO, open(write_fd, 'w')) # Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs, # our sigint handler will forward it only to the most recent pipe process. This makes sure pipe # processes close in the right order (most recent first). - kwargs: Dict[str, Any] = dict() + kwargs: dict[str, Any] = {} if sys.platform == 'win32': kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP else: @@ -2919,7 +2862,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: kwargs['executable'] = shell # For any stream that is a StdSim, we will use a pipe so we can capture its output - proc = subprocess.Popen( # type: ignore[call-overload] + proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602 statement.pipe_to, stdin=subproc_stdin, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] @@ -2932,20 +2875,17 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: # like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process # started OK, since the shell itself always starts. Therefore, we will wait a short time and check # if the pipe process is still running. - try: + with suppress(subprocess.TimeoutExpired): proc.wait(0.2) - except subprocess.TimeoutExpired: - pass # Check if the pipe process already exited if proc.returncode is not None: subproc_stdin.close() new_stdout.close() raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run') - else: - redir_saved_state.redirecting = True # type: ignore[unreachable] - cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) - sys.stdout = self.stdout = new_stdout + redir_saved_state.redirecting = True # type: ignore[unreachable] + cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) + sys.stdout = self.stdout = new_stdout elif statement.output: if statement.output_to: @@ -2990,7 +2930,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: return redir_saved_state def _restore_output(self, statement: Statement, saved_redir_state: utils.RedirectionSavedState) -> None: - """Handles restoring state after output redirection + """Handles restoring state after output redirection. :param statement: Statement object which contains the parsed input from the user :param saved_redir_state: contains information needed to restore state data @@ -3020,18 +2960,17 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec self._redirecting = saved_redir_state.saved_redirecting def cmd_func(self, command: str) -> Optional[CommandFunc]: - """ - Get the function for a command + """Get the function for a command. :param command: the name of the command Example: - ```py helpfunc = self.cmd_func('help') ``` helpfunc now contains a reference to the ``do_help`` method + """ func_name = constants.COMMAND_FUNC_PREFIX + command func = getattr(self, func_name, None) @@ -3082,14 +3021,13 @@ def default(self, statement: Statement) -> Optional[bool]: # type: ignore[overr self.history.append(statement) return self.do_shell(statement.command_and_args) - else: - err_msg = self.default_error.format(statement.command) - if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)): - err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}" + err_msg = self.default_error.format(statement.command) + if self.suggest_similar_command and (suggested_command := self._suggest_similar_command(statement.command)): + err_msg += f"\n{self.default_suggestion_message.format(suggested_command)}" - # Set apply_style to False so styles for default_error and default_suggestion_message are not overridden - self.perror(err_msg, apply_style=False) - return None + # Set apply_style to False so styles for default_error and default_suggestion_message are not overridden + self.perror(err_msg, apply_style=False) + return None def _suggest_similar_command(self, command: str) -> Optional[str]: return suggest_similar(command, self.get_visible_commands()) @@ -3098,7 +3036,7 @@ def read_input( self, prompt: str, *, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, completion_mode: utils.CompletionMode = utils.CompletionMode.NONE, preserve_quotes: bool = False, choices: Optional[Iterable[Any]] = None, @@ -3106,8 +3044,7 @@ def read_input( completer: Optional[CompleterFunc] = None, parser: Optional[argparse.ArgumentParser] = None, ) -> str: - """ - Read input from appropriate stdin value. Also supports tab completion and up-arrow history while + """Read input from appropriate stdin value. Also supports tab completion and up-arrow history while input is being entered. :param prompt: prompt to display to user @@ -3139,10 +3076,10 @@ def read_input( """ readline_configured = False saved_completer: Optional[CompleterFunc] = None - saved_history: Optional[List[str]] = None + saved_history: Optional[list[str]] = None def configure_readline() -> None: - """Configure readline tab completion and history""" + """Configure readline tab completion and history.""" nonlocal readline_configured nonlocal saved_completer nonlocal saved_history @@ -3198,7 +3135,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover readline_configured = True def restore_readline() -> None: - """Restore readline tab completion and history""" + """Restore readline tab completion and history.""" nonlocal readline_configured if not readline_configured or rl_type == RlType.NONE: # pragma: no cover return @@ -3232,31 +3169,29 @@ def restore_readline() -> None: sys.stdout.write(f'{prompt}{line}\n') # Otherwise read from self.stdin + elif self.stdin.isatty(): + # on a tty, print the prompt first, then read the line + self.poutput(prompt, end='') + self.stdout.flush() + line = self.stdin.readline() + if len(line) == 0: + line = 'eof' else: - if self.stdin.isatty(): - # on a tty, print the prompt first, then read the line - self.poutput(prompt, end='') - self.stdout.flush() - line = self.stdin.readline() - if len(line) == 0: - line = 'eof' + # we are reading from a pipe, read the line to see if there is + # anything there, if so, then decide whether to print the + # prompt or not + line = self.stdin.readline() + if len(line): + # we read something, output the prompt and the something + if self.echo: + self.poutput(f'{prompt}{line}') else: - # we are reading from a pipe, read the line to see if there is - # anything there, if so, then decide whether to print the - # prompt or not - line = self.stdin.readline() - if len(line): - # we read something, output the prompt and the something - if self.echo: - self.poutput(f'{prompt}{line}') - else: - line = 'eof' + line = 'eof' return line.rstrip('\r\n') def _read_command_line(self, prompt: str) -> str: - """ - Read command line from appropriate stdin + """Read command line from appropriate stdin. :param prompt: prompt to display to user :return: command line text of 'eof' if an EOFError was caught @@ -3277,8 +3212,7 @@ def _read_command_line(self, prompt: str) -> str: self.terminal_lock.acquire() def _set_up_cmd2_readline(self) -> _SavedReadlineSettings: - """ - Called at beginning of command loop to set up readline with cmd2-specific settings + """Called at beginning of command loop to set up readline with cmd2-specific settings. :return: Class containing saved readline settings """ @@ -3318,8 +3252,7 @@ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings: return readline_settings def _restore_readline(self, readline_settings: _SavedReadlineSettings) -> None: - """ - Called at end of command loop to restore saved readline settings + """Called at end of command loop to restore saved readline settings. :param readline_settings: the readline settings to restore """ @@ -3388,7 +3321,7 @@ def _cmdloop(self) -> None: # Preserve quotes since we are passing strings to other commands @with_argparser(alias_parser, preserve_quotes=True) def do_alias(self, args: argparse.Namespace) -> None: - """Manage aliases""" + """Manage aliases.""" # Call handler for whatever subcommand was selected handler = args.cmd2_handler.get() handler(args) @@ -3423,7 +3356,7 @@ def do_alias(self, args: argparse.Namespace) -> None: @as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower()) def _alias_create(self, args: argparse.Namespace) -> None: - """Create or overwrite an alias""" + """Create or overwrite an alias.""" self.last_result = False # Validate the alias name @@ -3473,7 +3406,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) def _alias_delete(self, args: argparse.Namespace) -> None: - """Delete aliases""" + """Delete aliases.""" self.last_result = True if args.all: @@ -3510,18 +3443,15 @@ def _alias_delete(self, args: argparse.Namespace) -> None: @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help) def _alias_list(self, args: argparse.Namespace) -> None: - """List some or all aliases as 'alias create' commands""" - self.last_result = {} # Dict[alias_name, alias_value] + """List some or all aliases as 'alias create' commands.""" + self.last_result = {} # dict[alias_name, alias_value] tokens_to_quote = constants.REDIRECTION_TOKENS tokens_to_quote.extend(self.statement_parser.terminators) - if args.names: - to_list = utils.remove_duplicates(args.names) - else: - to_list = sorted(self.aliases, key=self.default_sort_key) + to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.aliases, key=self.default_sort_key) - not_found: List[str] = [] + not_found: list[str] = [] for name in to_list: if name not in self.aliases: not_found.append(name) @@ -3556,7 +3486,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: # Preserve quotes since we are passing strings to other commands @with_argparser(macro_parser, preserve_quotes=True) def do_macro(self, args: argparse.Namespace) -> None: - """Manage macros""" + """Manage macros.""" # Call handler for whatever subcommand was selected handler = args.cmd2_handler.get() handler(args) @@ -3615,7 +3545,7 @@ def do_macro(self, args: argparse.Namespace) -> None: @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help) def _macro_create(self, args: argparse.Namespace) -> None: - """Create or overwrite a macro""" + """Create or overwrite a macro.""" self.last_result = False # Validate the macro name @@ -3660,8 +3590,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: return arg_nums.add(cur_num) - if cur_num > max_arg_num: - max_arg_num = cur_num + max_arg_num = max(max_arg_num, cur_num) arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False)) @@ -3709,7 +3638,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) def _macro_delete(self, args: argparse.Namespace) -> None: - """Delete macros""" + """Delete macros.""" self.last_result = True if args.all: @@ -3746,18 +3675,15 @@ def _macro_delete(self, args: argparse.Namespace) -> None: @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help) def _macro_list(self, args: argparse.Namespace) -> None: - """List some or all macros as 'macro create' commands""" - self.last_result = {} # Dict[macro_name, macro_value] + """List some or all macros as 'macro create' commands.""" + self.last_result = {} # dict[macro_name, macro_value] tokens_to_quote = constants.REDIRECTION_TOKENS tokens_to_quote.extend(self.statement_parser.terminators) - if args.names: - to_list = utils.remove_duplicates(args.names) - else: - to_list = sorted(self.macros, key=self.default_sort_key) + to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.macros, key=self.default_sort_key) - not_found: List[str] = [] + not_found: list[str] = [] for name in to_list: if name not in self.macros: not_found.append(name) @@ -3779,9 +3705,8 @@ def _macro_list(self, args: argparse.Namespace) -> None: for name in not_found: self.perror(f"Macro '{name}' not found") - def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """Completes the command argument of help""" - + def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: + """Completes the command argument of help.""" # Complete token against topics and visible commands topics = set(self.get_help_topics()) visible_commands = set(self.get_visible_commands()) @@ -3789,10 +3714,9 @@ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) return self.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]: - """Completes the subcommands argument of help""" - + 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 command = arg_tokens['command'][0] if not command: @@ -3824,7 +3748,7 @@ def complete_help_subcommands( @with_argparser(help_parser) def do_help(self, args: argparse.Namespace) -> None: - """List available commands or provide detailed help for a specific command""" + """List available commands or provide detailed help for a specific command.""" self.last_result = True if not args.command or args.verbose: @@ -3859,10 +3783,9 @@ def do_help(self, args: argparse.Namespace) -> None: self.perror(err_msg, apply_style=False) self.last_result = False - def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxcol: int) -> None: - """ - Print groups of commands and topics in columns and an optional header - Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters + def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxcol: int) -> None: + """Print groups of commands and topics in columns and an optional header + Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters. :param header: string to print above commands being printed :param cmds: list of topics to print @@ -3877,9 +3800,9 @@ def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxc self.columnize(cmds, maxcol - 1) self.poutput() - def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None: + def columnize(self, str_list: Optional[list[str]], display_width: int = 80) -> None: """Display a list of single-line strings as a compact set of columns. - Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters + Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters. Each column is only as wide as necessary. Columns are separated by two spaces (one was not legible enough). @@ -3923,10 +3846,7 @@ def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> N texts = [] for col in range(ncols): i = row + nrows * col - if i >= size: - x = "" - else: - x = str_list[i] + x = "" if i >= size else str_list[i] texts.append(x) while texts and not texts[-1]: del texts[-1] @@ -3935,7 +3855,7 @@ def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> N self.poutput(" ".join(texts)) def _help_menu(self, verbose: bool = False) -> None: - """Show a list of commands which help can be displayed for""" + """Show a list of commands which help can be displayed for.""" cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info() if not cmds_cats: @@ -3953,15 +3873,15 @@ def _help_menu(self, verbose: bool = False) -> None: self.print_topics(self.misc_header, help_topics, 15, 80) self.print_topics(self.undoc_header, cmds_undoc, 15, 80) - def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]: + def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]: # Get a sorted list of help topics help_topics = sorted(self.get_help_topics(), key=self.default_sort_key) # Get a sorted list of visible command names visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key) - cmds_doc: List[str] = [] - cmds_undoc: List[str] = [] - cmds_cats: Dict[str, List[str]] = {} + cmds_doc: list[str] = [] + cmds_undoc: list[str] = [] + cmds_cats: dict[str, list[str]] = {} for command in visible_commands: func = cast(CommandFunc, self.cmd_func(command)) has_help_func = False @@ -3984,8 +3904,8 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str cmds_undoc.append(command) return cmds_cats, cmds_doc, cmds_undoc, help_topics - def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None: - """Customized version of print_topics that can switch between verbose or traditional output""" + def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: + """Customized version of print_topics that can switch between verbose or traditional output.""" import io if cmds: @@ -4056,10 +3976,10 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None: @with_argparser(shortcuts_parser) def do_shortcuts(self, _: argparse.Namespace) -> None: - """List available shortcuts""" + """List available shortcuts.""" # Sort the shortcut tuples by name sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0])) - result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts) + result = "\n".join(f'{sc[0]}: {sc[1]}' for sc in sorted_shortcuts) self.poutput(f"Shortcuts for other commands:\n{result}") self.last_result = True @@ -4069,8 +3989,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: @with_argparser(eof_parser) def do_eof(self, _: argparse.Namespace) -> Optional[bool]: - """ - Called when Ctrl-D is pressed and calls quit with no arguments. + """Called when Ctrl-D is pressed and calls quit with no arguments. This can be overridden if quit should be called differently. """ self.poutput() @@ -4082,12 +4001,12 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]: @with_argparser(quit_parser) def do_quit(self, _: argparse.Namespace) -> Optional[bool]: - """Exit this application""" + """Exit this application.""" # Return True to stop the command loop self.last_result = True return True - def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any: + def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any: """Presents a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. @@ -4097,13 +4016,14 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p | a list of strings -> will be offered as options | a list of tuples -> interpreted as (value, text), so that the return value can differ from - the text advertised to the user""" - local_opts: Union[List[str], List[Tuple[Any, Optional[str]]]] + the text advertised to the user + """ + local_opts: Union[list[str], list[tuple[Any, Optional[str]]]] if isinstance(opts, str): - local_opts = cast(List[Tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) + local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split()))) else: local_opts = opts - fulloptions: List[Tuple[Any, Optional[str]]] = [] + fulloptions: list[tuple[Any, Optional[str]]] = [] for opt in local_opts: if isinstance(opt, str): fulloptions.append((opt, opt)) @@ -4113,7 +4033,7 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p except IndexError: fulloptions.append((opt[0], opt[0])) for idx, (_, text) in enumerate(fulloptions): - self.poutput(' %2d. %s' % (idx + 1, text)) + self.poutput(' %2d. %s' % (idx + 1, text)) # noqa: UP031 while True: try: @@ -4121,9 +4041,9 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p except EOFError: response = '' self.poutput() - except KeyboardInterrupt as ex: + except KeyboardInterrupt: self.poutput('^C') - raise ex + raise if not response: continue @@ -4137,9 +4057,9 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:") 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""" + 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: settable = self.settables[param] @@ -4192,7 +4112,7 @@ def complete_set_value( # 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) def do_set(self, args: argparse.Namespace) -> None: - """Set a settable parameter or show current settings of parameters""" + """Set a settable parameter or show current settings of parameters.""" self.last_result = False if not self.settables: @@ -4211,7 +4131,7 @@ def do_set(self, args: argparse.Namespace) -> None: try: orig_value = settable.get_value() settable.set_value(utils.strip_quotes(args.value)) - except Exception as ex: + except ValueError as ex: self.perror(f"Error setting {args.param}: {ex}") else: self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.get_value()!r}") @@ -4229,7 +4149,7 @@ def do_set(self, args: argparse.Namespace) -> None: max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show]) max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label)) - cols: List[Column] = [ + cols: list[Column] = [ Column(name_label, width=max_name_width), Column('Value', width=30), Column('Description', width=60), @@ -4239,7 +4159,7 @@ def do_set(self, args: argparse.Namespace) -> None: self.poutput(table.generate_header()) # Build the table and populate self.last_result - self.last_result = {} # Dict[settable_name, settable_value] + self.last_result = {} # dict[settable_name, settable_value] for param in sorted(to_show, key=self.default_sort_key): settable = self.settables[param] @@ -4256,11 +4176,11 @@ def do_set(self, args: argparse.Namespace) -> None: # Preserve quotes since we are passing these strings to the shell @with_argparser(shell_parser, preserve_quotes=True) def do_shell(self, args: argparse.Namespace) -> None: - """Execute a command as if at the OS prompt""" + """Execute a command as if at the OS prompt.""" import signal import subprocess - kwargs: Dict[str, Any] = dict() + kwargs: dict[str, Any] = {} # Set OS-specific parameters if sys.platform.startswith('win'): @@ -4282,7 +4202,7 @@ def do_shell(self, args: argparse.Namespace) -> None: kwargs['executable'] = shell # Create a list of arguments to shell - tokens = [args.command] + args.command_args + tokens = [args.command, *args.command_args] # Expand ~ where needed utils.expand_user_in_tokens(tokens) @@ -4292,7 +4212,7 @@ def do_shell(self, args: argparse.Namespace) -> None: # 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( # type: ignore[call-overload] + proc = subprocess.Popen( # type: ignore[call-overload] # noqa: S602 expanded_command, stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable] stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable] @@ -4313,8 +4233,7 @@ def do_shell(self, args: argparse.Namespace) -> None: @staticmethod def _reset_py_display() -> None: - """ - Resets the dynamic objects in the sys module that the py and ipy consoles fight over. + """Resets the dynamic objects in the sys module that the py and ipy consoles fight over. When a Python console starts it adopts certain display settings if they've already been set. If an ipy console has previously been run, then py uses its settings and ends up looking like an ipy console in terms of prompt and exception text. This method forces the Python @@ -4326,19 +4245,16 @@ def _reset_py_display() -> None: # Delete any prompts that have been set attributes = ['ps1', 'ps2', 'ps3'] for cur_attr in attributes: - try: + with suppress(KeyError): del sys.__dict__[cur_attr] - except KeyError: - pass # Reset functions sys.displayhook = sys.__displayhook__ sys.excepthook = sys.__excepthook__ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: - """ - Set up interactive Python shell environment - :return: Class containing saved up cmd2 environment + """Set up interactive Python shell environment + :return: Class containing saved up cmd2 environment. """ cmd2_env = _SavedCmd2Env() @@ -4400,8 +4316,7 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: return cmd2_env def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None: - """ - Restore cmd2 environment after exiting an interactive Python shell + """Restore cmd2 environment after exiting an interactive Python shell. :param cmd2_env: the environment settings to restore """ @@ -4437,8 +4352,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None: sys.modules['readline'] = cmd2_env.readline_module def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: - """ - Called by do_py() and do_run_pyscript(). + """Called by do_py() and do_run_pyscript(). If pyscript is None, then this function runs an interactive Python shell. Otherwise, it runs the pyscript file. @@ -4449,7 +4363,7 @@ def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: self.last_result = False def py_quit() -> None: - """Function callable from the interactive Python console to exit that environment""" + """Function callable from the interactive Python console to exit that environment.""" raise EmbeddedConsoleExit from .py_bridge import ( @@ -4511,7 +4425,7 @@ def py_quit() -> None: if py_code_to_run: try: interp.runcode(py_code_to_run) # type: ignore[arg-type] - except BaseException: + except BaseException: # noqa: BLE001, S110 # We don't care about any exception that happened in the Python code pass @@ -4534,7 +4448,7 @@ def py_quit() -> None: # Since quit() or exit() raise an EmbeddedConsoleExit, interact() exits before printing # the exitmsg. Therefore, we will not provide it one and print it manually later. interp.interact(banner=banner, exitmsg='') - except BaseException: + except BaseException: # noqa: BLE001, S110 # We don't care about any exception that happened in the interactive console pass finally: @@ -4556,9 +4470,8 @@ def py_quit() -> None: @with_argparser(py_parser) def do_py(self, _: argparse.Namespace) -> Optional[bool]: - """ - Run an interactive Python shell - :return: True if running of commands should stop + """Run an interactive Python shell + :return: True if running of commands should stop. """ # self.last_resort will be set by _run_python() return self._run_python() @@ -4571,8 +4484,7 @@ def do_py(self, _: argparse.Namespace) -> Optional[bool]: @with_argparser(run_pyscript_parser) def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: - """ - Run a Python script file inside the console + """Run a Python script file inside the console. :return: True if running of commands should stop """ @@ -4594,7 +4506,7 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: try: # Overwrite sys.argv to allow the script to take command line arguments - sys.argv = [args.script_path] + args.script_arguments + sys.argv = [args.script_path, *args.script_arguments] # self.last_resort will be set by _run_python() py_return = self._run_python(pyscript=args.script_path) @@ -4608,8 +4520,7 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: @with_argparser(ipython_parser) def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover - """ - Enter an interactive IPython shell + """Enter an interactive IPython shell. :return: True if running of commands should stop """ @@ -4617,11 +4528,11 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover # Detect whether IPython is installed try: - import traitlets.config.loader as TraitletsLoader # type: ignore[import] + import traitlets.config.loader as traitlets_loader # type: ignore[import] # Allow users to install ipython from a cmd2 prompt when needed and still have ipy command work try: - start_ipython # noqa F823 + start_ipython # noqa: F823 except NameError: from IPython import start_ipython # type: ignore[import] @@ -4658,7 +4569,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover local_vars['self'] = self # Configure IPython - config = TraitletsLoader.Config() # type: ignore + config = traitlets_loader.Config() config.InteractiveShell.banner2 = ( 'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n' f'Run CLI commands with: {self.py_bridge_name}("command ...")\n' @@ -4728,8 +4639,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover @with_argparser(history_parser) def do_history(self, args: argparse.Namespace) -> Optional[bool]: - """ - View, run, edit, save, or clear previously entered commands + """View, run, edit, save, or clear previously entered commands. :return: True if running of commands should stop """ @@ -4845,7 +4755,7 @@ def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryIte return history def _initialize_history(self, hist_file: str) -> None: - """Initialize history using history related attributes + """Initialize history using history related attributes. :param hist_file: optional path to persistent history file. If specified, then history from previous sessions will be included. Additionally, all history will be written @@ -4879,7 +4789,7 @@ def _initialize_history(self, hist_file: str) -> None: with open(hist_file, 'rb') as fobj: compressed_bytes = fobj.read() except FileNotFoundError: - compressed_bytes = bytes() + compressed_bytes = b"" except OSError as ex: self.perror(f"Cannot read persistent history file '{hist_file}': {ex}") return @@ -4898,11 +4808,11 @@ def _initialize_history(self, hist_file: str) -> None: try: import lzma as decompress_lib - decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,) + decompress_exceptions: tuple[type[Exception]] = (decompress_lib.LZMAError,) except ModuleNotFoundError: # pragma: no cover import bz2 as decompress_lib # type: ignore[no-redef] - decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef] + decompress_exceptions: tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef] try: history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8') @@ -4938,7 +4848,7 @@ def _initialize_history(self, hist_file: str) -> None: readline.add_history(formatted_command) def _persist_history(self) -> None: - """Write history out to the persistent history file as compressed JSON""" + """Write history out to the persistent history file as compressed JSON.""" if not self.persistent_history_file: return @@ -4959,12 +4869,12 @@ def _persist_history(self) -> None: def _generate_transcript( self, - history: Union[List[HistoryItem], List[str]], + history: Union[list[HistoryItem], list[str]], transcript_file: str, *, add_to_history: bool = True, ) -> None: - """Generate a transcript file from a given history of commands""" + """Generate a transcript file from a given history of commands.""" self.last_result = False # Validate the transcript file path to make sure directory exists and write access is available @@ -4998,7 +4908,7 @@ def _generate_transcript( first = True command = '' if isinstance(history_item, HistoryItem): - history_item = history_item.raw + history_item = history_item.raw # noqa: PLW2901 for line in history_item.splitlines(): if first: command += f"{self.prompt}{line}\n" @@ -5048,10 +4958,7 @@ def _generate_transcript( self.perror(f"Error saving transcript file '{transcript_path}': {ex}") else: # and let the user know what we did - if commands_run == 1: - plural = 'command and its output' - else: - plural = 'commands and their outputs' + plural = 'command and its output' if commands_run == 1 else 'commands and their outputs' self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'") self.last_result = True @@ -5070,20 +4977,18 @@ def _generate_transcript( @with_argparser(edit_parser) def do_edit(self, args: argparse.Namespace) -> None: - """Run a text editor and optionally open a file with it""" - + """Run a text editor and optionally open a file with it.""" # self.last_result will be set by do_shell() which is called by run_editor() self.run_editor(args.file_path) def run_editor(self, file_path: Optional[str] = None) -> None: - """ - Run a text editor and optionally open a file with it + """Run a text editor and optionally open a file with it. :param file_path: optional path of the file to edit. Defaults to None. :raises EnvironmentError: if self.editor is not set """ if not self.editor: - raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.") + raise OSError("Please use 'set editor' to specify your text editing program of choice.") command = utils.quote_string(os.path.expanduser(self.editor)) if file_path: @@ -5096,8 +5001,7 @@ def _current_script_dir(self) -> Optional[str]: """Accessor to get the current script directory from the _script_dir LIFO queue.""" if self._script_dir: return self._script_dir[-1] - else: - return None + return None run_script_description = ( "Run commands in script file that is encoded as either ASCII or UTF-8 text\n" @@ -5198,8 +5102,7 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: @with_argparser(relative_run_script_parser) def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: - """ - Run commands in script file that is encoded as either ASCII or UTF-8 text + """Run commands in script file that is encoded as either ASCII or UTF-8 text. :return: True if running of commands should stop """ @@ -5210,7 +5113,7 @@ def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: # self.last_result will be set by do_run_script() return self.do_run_script(utils.quote_string(relative_path)) - def _run_transcript_tests(self, transcript_paths: List[str]) -> None: + def _run_transcript_tests(self, transcript_paths: list[str]) -> None: """Runs transcript tests for provided file(s). This is called when either -t is provided on the command line or the transcript_files argument is provided @@ -5273,8 +5176,7 @@ class TestMyAppCase(Cmd2TestCase): self.exit_code = 1 def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover - """ - Display an important message to the user while they are at a command line prompt. + """Display an important message to the user while they are at a command line prompt. To the user it appears as if an alert message is printed above the prompt and their current input text and cursor location is left alone. @@ -5345,8 +5247,7 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: raise RuntimeError("another thread holds terminal_lock") def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover - """ - Update the command line prompt while the user is still typing at it. + """Update the command line prompt while the user is still typing at it. This is good for alerting the user to system changes dynamically in between commands. For instance you could alter the color of the prompt to indicate a system status or increase a @@ -5365,8 +5266,7 @@ def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover self.async_alert('', new_prompt) def async_refresh_prompt(self) -> None: # pragma: no cover - """ - Refresh the oncreen prompt to match self.prompt. + """Refresh the oncreen prompt to match self.prompt. One case where the onscreen prompt and self.prompt can get out of sync is when async_alert() is called while a user is in search mode (e.g. Ctrl-r). @@ -5392,8 +5292,7 @@ def need_prompt_refresh(self) -> bool: # pragma: no cover @staticmethod def set_window_title(title: str) -> None: # pragma: no cover - """ - Set the terminal window title. + """Set the terminal window title. NOTE: This function writes to stderr. Therefore, if you call this during a command run by a pyscript, the string which updates the title will appear in that command's CommandResult.stderr data. @@ -5411,8 +5310,7 @@ def set_window_title(title: str) -> None: # pragma: no cover pass def enable_command(self, command: str) -> None: - """ - Enable a command by restoring its functions + """Enable a command by restoring its functions. :param command: the command being enabled """ @@ -5444,8 +5342,7 @@ def enable_command(self, command: str) -> None: del self.disabled_commands[command] def enable_category(self, category: str) -> None: - """ - Enable an entire category of commands + """Enable an entire category of commands. :param category: the category to enable """ @@ -5455,8 +5352,7 @@ def enable_category(self, category: str) -> None: self.enable_command(cmd_name) def disable_command(self, command: str, message_to_print: str) -> None: - """ - Disable a command and overwrite its functions + """Disable a command and overwrite its functions. :param command: the command being disabled :param message_to_print: what to print when this command is run or help is called on it while disabled @@ -5512,8 +5408,7 @@ def disable_category(self, category: str, message_to_print: str) -> None: self.disable_command(cmd_name, message_to_print) def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_kwargs: Any) -> None: - """ - Report when a disabled command has been run or had help called on it + """Report when a disabled command has been run or had help called on it. :param _args: not used :param message_to_print: the message reporting that the command is disabled @@ -5598,13 +5493,13 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] # ### def _initialize_plugin_system(self) -> None: - """Initialize the plugin system""" - self._preloop_hooks: List[Callable[[], None]] = [] - self._postloop_hooks: List[Callable[[], None]] = [] - self._postparsing_hooks: List[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = [] - self._precmd_hooks: List[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = [] - self._postcmd_hooks: List[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = [] - self._cmdfinalization_hooks: List[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = [] + """Initialize the plugin system.""" + self._preloop_hooks: list[Callable[[], None]] = [] + self._postloop_hooks: list[Callable[[], None]] = [] + self._postparsing_hooks: list[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = [] + self._precmd_hooks: list[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = [] + self._postcmd_hooks: list[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = [] + self._cmdfinalization_hooks: list[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = [] @classmethod def _validate_callable_param_count(cls, func: Callable[..., Any], count: int) -> None: @@ -5637,17 +5532,17 @@ def register_postloop_hook(self, func: Callable[[], None]) -> None: @classmethod def _validate_postparsing_callable(cls, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None: - """Check parameter and return types for postparsing hooks""" + """Check parameter and return types for postparsing hooks.""" cls._validate_callable_param_count(cast(Callable[..., Any], func), 1) signature = inspect.signature(func) - _, param = list(signature.parameters.items())[0] + _, param = next(iter(signature.parameters.items())) if param.annotation != plugin.PostparsingData: raise TypeError(f"{func.__name__} must have one parameter declared with type 'cmd2.plugin.PostparsingData'") if signature.return_annotation != plugin.PostparsingData: raise TypeError(f"{func.__name__} must declare return a return type of 'cmd2.plugin.PostparsingData'") 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""" + """Register a function to be called after parsing user input but before running the command.""" self._validate_postparsing_callable(func) self._postparsing_hooks.append(func) @@ -5655,14 +5550,14 @@ def register_postparsing_hook(self, func: Callable[[plugin.PostparsingData], plu @classmethod def _validate_prepostcmd_hook( - cls, func: Callable[[CommandDataType], CommandDataType], data_type: Type[CommandDataType] + cls, func: Callable[[CommandDataType], CommandDataType], data_type: type[CommandDataType] ) -> None: """Check parameter and return types for pre and post command hooks.""" signature = inspect.signature(func) # validate that the callable has the right number of parameters cls._validate_callable_param_count(cast(Callable[..., Any], func), 1) # validate the parameter has the right annotation - paramname = list(signature.parameters.keys())[0] + paramname = next(iter(signature.parameters.keys())) param = signature.parameters[paramname] if param.annotation != data_type: raise TypeError(f'argument 1 of {func.__name__} has incompatible type {param.annotation}, expected {data_type}') @@ -5691,7 +5586,7 @@ def _validate_cmdfinalization_callable( """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] + _, param = next(iter(signature.parameters.items())) if param.annotation != plugin.CommandFinalizationData: raise TypeError(f"{func.__name__} must have one parameter declared with type {plugin.CommandFinalizationData}") if signature.return_annotation != plugin.CommandFinalizationData: @@ -5709,16 +5604,15 @@ def _resolve_func_self( cmd_support_func: Callable[..., Any], cmd_self: Union[CommandSet, 'Cmd', None], ) -> Optional[object]: - """ - Attempt to resolve a candidate instance to pass as 'self' for an unbound class method that was + """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 - instance of each type, using type is a reasonably safe way to resolve the correct object instance + instance of each type, using type is a reasonably safe way to resolve the correct object instance. :param cmd_support_func: command support function. This could be a completer or namespace provider :param cmd_self: The `self` associated with the command or subcommand """ # figure out what class the command support function was defined in - func_class: Optional[Type[Any]] = get_defining_class(cmd_support_func) + func_class: Optional[type[Any]] = get_defining_class(cmd_support_func) # Was there a defining class identified? If so, is it a sub-class of CommandSet? if func_class is not None and issubclass(func_class, CommandSet): @@ -5729,7 +5623,7 @@ def _resolve_func_self( # 2. Do any of the registered CommandSets in the Cmd2 application exactly match the type? # 3. Is there a registered CommandSet that is is the only matching subclass? - func_self: Optional[Union[CommandSet, 'Cmd']] + func_self: Optional[Union[CommandSet, Cmd]] # check if the command's CommandSet is a sub-class of the support function's defining class if isinstance(cmd_self, func_class): @@ -5738,7 +5632,7 @@ def _resolve_func_self( else: # Search all registered CommandSets func_self = None - candidate_sets: List[CommandSet] = [] + candidate_sets: list[CommandSet] = [] for installed_cmd_set in self._installed_command_sets: if type(installed_cmd_set) == func_class: # noqa: E721 # Case 2: CommandSet is an exact type match for the function's CommandSet @@ -5752,5 +5646,4 @@ def _resolve_func_self( # Case 3: There exists exactly 1 CommandSet that is a sub-class match of the function's CommandSet func_self = candidate_sets[0] return func_self - else: - return self + return self diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 5bac8ef32..941fe0337 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -1,15 +1,9 @@ -# coding=utf-8 -""" -Supports the definition of commands in separate classes to be composed into cmd2.Cmd -""" +"""Supports the definition of commands in separate classes to be composed into cmd2.Cmd.""" +from collections.abc import Callable, Mapping from typing import ( TYPE_CHECKING, - Callable, - Dict, - Mapping, Optional, - Type, TypeVar, ) @@ -31,12 +25,11 @@ #: Further refinements are needed to define the input parameters CommandFunc = Callable[..., Optional[bool]] -CommandSetType = TypeVar('CommandSetType', bound=Type['CommandSet']) +CommandSetType = TypeVar('CommandSetType', bound=type['CommandSet']) def with_default_category(category: str, *, heritable: bool = True) -> Callable[[CommandSetType], CommandSetType]: - """ - Decorator that applies a category to all ``do_*`` command methods in a class that do not already + """Decorator that applies a category to all ``do_*`` command methods in a class that do not already have a category specified. CommandSets that are decorated by this with `heritable` set to True (default) will set a class attribute that is @@ -85,9 +78,8 @@ def decorate_class(cls: CommandSetType) -> CommandSetType: return decorate_class -class CommandSet(object): - """ - Base class for defining sets of commands to load in cmd2. +class CommandSet: + """Base class for defining sets of commands to load in cmd2. ``with_default_category`` can be used to apply a default category to all commands in the CommandSet. @@ -100,13 +92,12 @@ def __init__(self) -> None: # accessed by child classes using the self._cmd property. self.__cmd_internal: Optional[cmd2.Cmd] = None - self._settables: Dict[str, Settable] = {} + self._settables: dict[str, Settable] = {} self._settable_prefix = self.__class__.__name__ @property def _cmd(self) -> 'cmd2.Cmd': - """ - Property for child classes to access self.__cmd_internal. + """Property for child classes to access self.__cmd_internal. Using this property ensures that self.__cmd_internal has been set and it tells type checkers that it's no longer a None type. @@ -121,8 +112,7 @@ def _cmd(self) -> 'cmd2.Cmd': return self.__cmd_internal def on_register(self, cmd: 'cmd2.Cmd') -> None: - """ - Called by cmd2.Cmd as the first step to registering a CommandSet. The commands defined in this class have + """Called by cmd2.Cmd as the first step to registering a CommandSet. The commands defined in this class have not been added to the CLI object at this point. Subclasses can override this to perform any initialization requiring access to the Cmd object (e.g. configure commands and their parsers based on CLI state data). @@ -135,23 +125,18 @@ def on_register(self, cmd: 'cmd2.Cmd') -> None: raise CommandSetRegistrationError('This CommandSet has already been registered') def on_registered(self) -> None: - """ - Called by cmd2.Cmd after a CommandSet is registered and all its commands have been added to the CLI. + """Called by cmd2.Cmd after a CommandSet is registered and all its commands have been added to the CLI. Subclasses can override this to perform custom steps related to the newly added commands (e.g. setting them to a disabled state). """ - pass def on_unregister(self) -> None: - """ - Called by ``cmd2.Cmd`` as the first step to unregistering a CommandSet. Subclasses can override this to + """Called by ``cmd2.Cmd`` as the first step to unregistering a CommandSet. Subclasses can override this to perform any cleanup steps which require their commands being registered in the CLI. """ - pass def on_unregistered(self) -> None: - """ - Called by ``cmd2.Cmd`` after a CommandSet has been unregistered and all its commands removed from the CLI. + """Called by ``cmd2.Cmd`` after a CommandSet has been unregistered and all its commands removed from the CLI. Subclasses can override this to perform remaining cleanup steps. """ self.__cmd_internal = None @@ -165,24 +150,22 @@ def settables(self) -> Mapping[str, Settable]: return self._settables def add_settable(self, settable: Settable) -> None: - """ - Convenience method to add a settable parameter to the CommandSet + """Convenience method to add a settable parameter to the CommandSet. :param settable: Settable object being added """ if self.__cmd_internal is not None: if not self._cmd.always_prefix_settables: - if settable.name in self._cmd.settables.keys() and settable.name not in self._settables.keys(): + if settable.name in self._cmd.settables and settable.name not in self._settables: raise KeyError(f'Duplicate settable: {settable.name}') else: prefixed_name = f'{self._settable_prefix}.{settable.name}' - if prefixed_name in self._cmd.settables.keys() and settable.name not in self._settables.keys(): + if prefixed_name in self._cmd.settables and settable.name not in self._settables: raise KeyError(f'Duplicate settable: {settable.name}') self._settables[settable.name] = settable def remove_settable(self, name: str) -> None: - """ - Convenience method for removing a settable parameter from the CommandSet + """Convenience method for removing a settable parameter from the CommandSet. :param name: name of the settable being removed :raises KeyError: if the Settable matches this name @@ -193,8 +176,7 @@ def remove_settable(self, name: str) -> None: raise KeyError(name + " is not a settable parameter") def sigint_handler(self) -> bool: - """ - Handle a SIGINT that occurred for a command in this CommandSet. + """Handle a SIGINT that occurred for a command in this CommandSet. :return: True if this completes the interrupt handling and no KeyboardInterrupt will be raised. False to raise a KeyboardInterrupt. diff --git a/cmd2/constants.py b/cmd2/constants.py index ebac5da95..3306f6fc8 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -1,5 +1,3 @@ -# -# coding=utf-8 """This module contains constants used throughout ``cmd2``.""" # Unless documented in https://cmd2.readthedocs.io/en/latest/api/index.html diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 343beaa2e..ca9fdba28 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -1,17 +1,11 @@ -# coding=utf-8 -"""Decorators for ``cmd2`` commands""" +"""Decorators for ``cmd2`` commands.""" import argparse +from collections.abc import Callable, Sequence from typing import ( TYPE_CHECKING, Any, - Callable, - Dict, - List, Optional, - Sequence, - Tuple, - Type, TypeVar, Union, ) @@ -44,7 +38,6 @@ def with_category(category: str) -> Callable[[CommandFunc], CommandFunc]: be grouped when displaying the list of commands. Example: - ```py class MyApp(cmd2.Cmd): @cmd2.with_category('Text Functions') @@ -54,6 +47,7 @@ def do_echo(self, args) For an alternative approach to categorizing commands using a function, see [cmd2.utils.categorize][] + """ def cat_decorator(func: CommandFunc) -> CommandFunc: @@ -68,7 +62,7 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet]) -CommandParentType = TypeVar('CommandParentType', bound=Union[Type['cmd2.Cmd'], Type[CommandSet]]) +CommandParentType = TypeVar('CommandParentType', bound=Union[type['cmd2.Cmd'], type[CommandSet]]) RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Union[Statement, str]], Optional[bool]] @@ -79,12 +73,11 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: # in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be # found we can swap out the statement with each decorator's specific parameters ########################## -def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]: - """ - Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found +def _parse_positionals(args: tuple[Any, ...]) -> tuple['cmd2.Cmd', Union[Statement, str]]: + """Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found Assumes that we will find cmd2.Cmd followed by the command statement object or string. :arg args: The positional arguments to inspect - :return: The cmd2.Cmd reference and the command line statement + :return: The cmd2.Cmd reference and the command line statement. """ for pos, arg in enumerate(args): from cmd2 import ( @@ -93,7 +86,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme if isinstance(arg, (Cmd, CommandSet)) and len(args) > pos + 1: if isinstance(arg, CommandSet): - arg = arg._cmd + arg = arg._cmd # noqa: PLW2901 next_arg = args[pos + 1] if isinstance(next_arg, (Statement, str)): return arg, args[pos + 1] @@ -103,9 +96,8 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found') -def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> List[Any]: - """ - Helper function for cmd2 decorators to swap the Statement parameter with one or more decorator-specific parameters +def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> list[Any]: + """Helper function for cmd2 decorators to swap the Statement parameter with one or more decorator-specific parameters. :param args: The original positional arguments :param search_arg: The argument to search for (usually the Statement) @@ -120,13 +112,13 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> #: Function signature for a command function that accepts a pre-processed argument list from user input #: and optionally returns a boolean -ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]] +ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, list[str]], Optional[bool]] #: Function signature for a command function that accepts a pre-processed argument list from user input #: and returns a boolean -ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool] +ArgListCommandFuncBoolReturn = Callable[[CommandParent, list[str]], bool] #: Function signature for a command function that accepts a pre-processed argument list from user input #: and returns Nothing -ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None] +ArgListCommandFuncNoneReturn = Callable[[CommandParent, list[str]], None] #: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list ArgListCommandFunc = Union[ @@ -144,8 +136,7 @@ def with_argument_list( RawCommandFuncOptionalBoolReturn[CommandParent], Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]], ]: - """ - A decorator to alter the arguments passed to a ``do_*`` method. Default + """A decorator to alter the arguments passed to a ``do_*`` method. Default passes a string of whatever the user typed. With this decorator, the decorated method will receive a list of arguments parsed from user input. @@ -161,12 +152,12 @@ class MyApp(cmd2.Cmd): def do_echo(self, arglist): self.poutput(' '.join(arglist) ``` + """ import functools def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]: - """ - Decorator function that ingests an Argument List function and returns a raw command function. + """Decorator function that ingests an Argument List function and returns a raw command function. The returned function will process the raw input into an argument list to be passed to the wrapped function. :param func: The defined argument list command function @@ -175,8 +166,7 @@ def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOpti @functools.wraps(func) def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: - """ - Command function wrapper which translates command line into an argument list and calls actual command function + """Command function wrapper which translates command line into an argument list and calls actual command function. :param args: All positional arguments to this function. We're expecting there to be: cmd2_app, statement: Union[Statement, str] @@ -195,13 +185,11 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: if callable(func_arg): return arg_decorator(func_arg) - else: - return arg_decorator + return arg_decorator def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: - """ - Recursively set prog attribute of a parser and all of its subparsers so that the root command + """Recursively set prog attribute of a parser and all of its subparsers so that the root command is a command name and not sys.argv[0]. :param parser: the parser being edited @@ -209,7 +197,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog - req_args: List[str] = [] + req_args: list[str] = [] # Set the prog value for the parser's subcommands for action in parser._actions: @@ -245,24 +233,24 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: break # Need to save required args so they can be prepended to the subcommand usage - elif action.required: + if action.required: req_args.append(action.dest) #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and optionally return a boolean ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]] -ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], Optional[bool]] +ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], Optional[bool]] #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and return a boolean ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool] -ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], bool] +ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], bool] #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and return nothing ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None] -ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, List[str]], None] +ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, list[str]], None] #: Aggregate of all accepted function signatures for an argparse command function ArgparseCommandFunc = Union[ @@ -301,7 +289,6 @@ def with_argparser( parsing the command line. This can be useful if the command function needs to know the command line. Example: - ```py parser = cmd2.Cmd2ArgumentParser() parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') @@ -331,12 +318,12 @@ def do_argprint(self, args, unknown): self.poutput(f'args: {args!r}') self.poutput(f'unknowns: {unknown}') ``` + """ import functools def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]: - """ - Decorator function that ingests an Argparse Command Function and returns a raw command function. + """Decorator function that ingests an Argparse Command Function and returns a raw command function. The returned function will process the raw input into an argparse Namespace to be passed to the wrapped function. :param func: The defined argparse command function @@ -344,10 +331,9 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOpt """ @functools.wraps(func) - def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: - """ - Command function wrapper which translates command line into argparse Namespace and calls actual - command function + def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: + """Command function wrapper which translates command line into argparse Namespace and calls actual + command function. :param args: All positional arguments to this function. We're expecting there to be: cmd2_app, statement: Union[Statement, str] @@ -377,7 +363,7 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]: namespace = ns_provider(provider_self if provider_self is not None else cmd2_app) try: - new_args: Union[Tuple[argparse.Namespace], Tuple[argparse.Namespace, List[str]]] + new_args: Union[tuple[argparse.Namespace], tuple[argparse.Namespace, list[str]]] if with_unknown_args: new_args = arg_parser.parse_known_args(parsed_arglist, namespace) else: @@ -421,11 +407,10 @@ def as_subcommand_to( Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod ], *, - help: Optional[str] = None, - aliases: Optional[List[str]] = None, + help: Optional[str] = None, # noqa: A002 + aliases: Optional[list[str]] = None, ) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - """ - Tag this method as a subcommand to an existing argparse decorated command. + """Tag this method as a subcommand to an existing argparse decorated command. :param command: Command Name. Space-delimited subcommands may optionally be specified :param subcommand: Subcommand name @@ -444,7 +429,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFu setattr(func, constants.SUBCMD_ATTR_NAME, subcommand) # Keyword arguments for subparsers.add_parser() - add_parser_kwargs: Dict[str, Any] = dict() + add_parser_kwargs: dict[str, Any] = {} if help is not None: add_parser_kwargs['help'] = help if aliases: diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index c07100134..0e9e9ce4c 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -1,47 +1,34 @@ -# coding=utf-8 -"""Custom exceptions for cmd2""" +"""Custom exceptions for cmd2.""" -from typing import ( - Any, -) +from typing import Any ############################################################################################################ # 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 +class SkipPostcommandHooks(Exception): # noqa: N818 + """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 - class Cmd2ArgparseError(SkipPostcommandHooks): - """ - A ``SkipPostcommandHooks`` exception for when a command fails to parse its arguments. + """A ``SkipPostcommandHooks`` exception for when a command fails to parse its arguments. Normally argparse raises a SystemExit exception in these cases. To avoid stopping the command 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 - class CommandSetRegistrationError(Exception): - """ - Exception that can be thrown when an error occurs while a CommandSet is being added or removed + """Exception that can be thrown when an error occurs while a CommandSet is being added or removed from a cmd2 application. """ - pass - class CompletionError(Exception): - """ - Raised during tab completion operations to report any sort of error you want printed. This can also be used + """Raised during tab completion operations to report any sort of error you want printed. This can also be used just to display a message, even if it's not an error. For instance, ArgparseCompleter raises CompletionErrors to display tab completion hints and sets apply_style to False so hints aren't colored like error text. @@ -53,8 +40,7 @@ class CompletionError(Exception): """ def __init__(self, *args: Any, apply_style: bool = True) -> None: - """ - Initializer for CompletionError + """Initializer for CompletionError :param apply_style: If True, then ansi.style_error will be applied to the message text when printed. Set to False in cases where the message text already has the desired style. Defaults to True. @@ -64,16 +50,14 @@ def __init__(self, *args: Any, apply_style: bool = True) -> None: super().__init__(*args) -class PassThroughException(Exception): - """ - Normally all unhandled exceptions raised during commands get printed to the user. +class PassThroughException(Exception): # noqa: N818 + """Normally all unhandled exceptions raised during commands get printed to the user. This class is used to wrap an exception that should be raised instead of printed. """ def __init__(self, *args: Any, wrapped_ex: BaseException) -> None: - """ - Initializer for PassThroughException - :param wrapped_ex: the exception that will be raised + """Initializer for PassThroughException + :param wrapped_ex: the exception that will be raised. """ self.wrapped_ex = wrapped_ex super().__init__(*args) @@ -85,24 +69,16 @@ def __init__(self, *args: Any, wrapped_ex: BaseException) -> None: class Cmd2ShlexError(Exception): - """Raised when shlex fails to parse a command line string in StatementParser""" - - pass + """Raised when shlex fails to parse a command line string in StatementParser.""" class EmbeddedConsoleExit(SystemExit): """Custom exception class for use with the py command.""" - pass - -class EmptyStatement(Exception): +class EmptyStatement(Exception): # noqa: N818 """Custom exception class for handling behavior when the user just presses .""" - pass - class RedirectionError(Exception): - """Custom exception class for when redirecting or piping output fails""" - - pass + """Custom exception class for when redirecting or piping output fails.""" diff --git a/cmd2/history.py b/cmd2/history.py index 289615c85..05d79a40a 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -1,22 +1,16 @@ -# coding=utf-8 -""" -History management classes -""" +"""History management classes.""" import json import re from collections import ( OrderedDict, ) +from collections.abc import Callable, Iterable from dataclasses import ( dataclass, ) from typing import ( Any, - Callable, - Dict, - Iterable, - List, Optional, Union, overload, @@ -32,8 +26,7 @@ def single_line_format(statement: Statement) -> str: - """ - Format a command line to display on a single line. + """Format a command line to display on a single line. Spaces and newlines in quotes are preserved so those strings will span multiple lines. @@ -71,7 +64,7 @@ def single_line_format(statement: Statement) -> str: @dataclass(frozen=True) class HistoryItem: - """Class used to represent one command in the history list""" + """Class used to represent one command in the history list.""" _listformat = ' {:>4} {}' _ex_listformat = ' {:>4}x {}' @@ -82,7 +75,7 @@ class HistoryItem: statement: Statement def __str__(self) -> str: - """A convenient human-readable representation of the history item""" + """A convenient human-readable representation of the history item.""" return self.statement.raw @property @@ -96,7 +89,7 @@ def raw(self) -> str: @property def expanded(self) -> str: """Return the command as run which includes shortcuts and aliases resolved - plus any changes made in hooks + plus any changes made in hooks. Proxy property for ``self.statement.expanded_command_line`` """ @@ -121,10 +114,7 @@ def pr(self, idx: int, script: bool = False, expanded: bool = False, verbose: bo if raw != expanded_command: ret_str += '\n' + self._ex_listformat.format(idx, expanded_command) else: - if expanded: - ret_str = self.expanded - else: - ret_str = single_line_format(self.statement).rstrip() + ret_str = self.expanded if expanded else single_line_format(self.statement).rstrip() # Display a numbered list if not writing to a script if not script: @@ -132,14 +122,13 @@ def pr(self, idx: int, script: bool = False, expanded: bool = False, verbose: bo return ret_str - def to_dict(self) -> Dict[str, Any]: - """Utility method to convert this HistoryItem into a dictionary for use in persistent JSON history files""" + def to_dict(self) -> dict[str, Any]: + """Utility method to convert this HistoryItem into a dictionary for use in persistent JSON history files.""" return {HistoryItem._statement_field: self.statement.to_dict()} @staticmethod - def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem': - """ - Utility method to restore a HistoryItem from a dictionary + def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem': + """Utility method to restore a HistoryItem from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) :return: HistoryItem object @@ -149,7 +138,7 @@ def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem': return HistoryItem(Statement.from_dict(statement_dict)) -class History(List[HistoryItem]): +class History(list[HistoryItem]): """A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods for searching and managing the list. @@ -169,7 +158,7 @@ class to gain access to the historical record. _history_items_field = 'history_items' def __init__(self, seq: Iterable[HistoryItem] = ()) -> None: - super(History, self).__init__(seq) + super().__init__(seq) self.session_start_index = 0 def start_session(self) -> None: @@ -196,7 +185,7 @@ def append(self, new: Union[Statement, HistoryItem]) -> None: and added to the end of the list """ history_item = HistoryItem(new) if isinstance(new, Statement) else new - super(History, self).append(history_item) + super().append(history_item) def clear(self) -> None: """Remove all items from the History list.""" @@ -211,10 +200,9 @@ def get(self, index: int) -> HistoryItem: """ if index == 0: raise IndexError('The first command in history is command 1.') - elif index < 0: + if index < 0: return self[index] - else: - return self[index - 1] + return self[index - 1] # This regular expression parses input for the span() method. There are five parts: # @@ -243,7 +231,7 @@ def get(self, index: int) -> HistoryItem: spanpattern = re.compile(r'^\s*(?P-?[1-9]\d*)?(?P:|(\.{2,}))(?P-?[1-9]\d*)?\s*$') def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': - """Return a slice of the History list + """Return a slice of the History list. :param span: string containing an index or a slice :param include_persisted: if True, then retrieve full results including from persisted history @@ -292,7 +280,7 @@ def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, return self._build_result_dictionary(start, end) def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': - """Find history items which contain a given string + """Find history items which contain a given string. :param search: the string to search for :param include_persisted: if True, then search full history including persisted history @@ -301,7 +289,7 @@ def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDi """ def isin(history_item: HistoryItem) -> bool: - """filter function for string search of history""" + """Filter function for string search of history.""" sloppy = utils.norm_fold(search) inraw = sloppy in utils.norm_fold(history_item.raw) inexpanded = sloppy in utils.norm_fold(history_item.expanded) @@ -311,7 +299,7 @@ def isin(history_item: HistoryItem) -> bool: return self._build_result_dictionary(start, len(self), isin) def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': - """Find history items which match a given regular expression + """Find history items which match a given regular expression. :param regex: the regular expression to search for. :param include_persisted: if True, then search full history including persisted history @@ -324,14 +312,14 @@ def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedD finder = re.compile(regex, re.DOTALL | re.MULTILINE) def isin(hi: HistoryItem) -> bool: - """filter function for doing a regular expression search of history""" + """Filter function for doing a regular expression search of history.""" return bool(finder.search(hi.raw) or finder.search(hi.expanded)) start = 0 if include_persisted else self.session_start_index return self._build_result_dictionary(start, len(self), isin) def truncate(self, max_length: int) -> None: - """Truncate the length of the history, dropping the oldest items if necessary + """Truncate the length of the history, dropping the oldest items if necessary. :param max_length: the maximum length of the history, if negative, all history items will be deleted @@ -347,10 +335,9 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None ) -> 'OrderedDict[int, HistoryItem]': - """ - Build history search results + """Build history search results :param start: start index to search from - :param end: end index to stop searching (exclusive) + :param end: end index to stop searching (exclusive). """ results: OrderedDict[int, HistoryItem] = OrderedDict() for index in range(start, end): @@ -359,7 +346,7 @@ def _build_result_dictionary( return results def to_json(self) -> str: - """Utility method to convert this History into a JSON string for use in persistent history files""" + """Utility method to convert this History into a JSON string for use in persistent history files.""" json_dict = { History._history_version_field: History._history_version, History._history_items_field: [hi.to_dict() for hi in self], @@ -368,8 +355,7 @@ def to_json(self) -> str: @staticmethod def from_json(history_json: str) -> 'History': - """ - Utility method to restore History from a JSON string + """Utility method to restore History from a JSON string. :param history_json: history data as JSON string (generated using to_json()) :return: History object diff --git a/cmd2/parsing.py b/cmd2/parsing.py index c42ac22b3..e77c81bb9 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -1,20 +1,15 @@ -# -# -*- coding: utf-8 -*- -"""Statement parsing classes for cmd2""" +"""Statement parsing classes for cmd2.""" import re import shlex +from collections.abc import Iterable from dataclasses import ( dataclass, field, ) from typing import ( Any, - Dict, - Iterable, - List, Optional, - Tuple, Union, ) @@ -27,9 +22,8 @@ ) -def shlex_split(str_to_split: str) -> List[str]: - """ - A wrapper around shlex.split() that uses cmd2's preferred arguments. +def shlex_split(str_to_split: str) -> list[str]: + """A wrapper around shlex.split() that uses cmd2's preferred arguments. This allows other classes to easily call split() the same way StatementParser does. :param str_to_split: the string being split @@ -40,10 +34,9 @@ def shlex_split(str_to_split: str) -> List[str]: @dataclass(frozen=True) class MacroArg: - """ - Information used to replace or unescape arguments in a macro value when the macro is resolved + """Information used to replace or unescape arguments in a macro value when the macro is resolved Normal argument syntax: {5} - Escaped argument syntax: {{5}} + Escaped argument syntax: {{5}}. """ # The starting index of this argument in the macro value @@ -73,7 +66,7 @@ class MacroArg: @dataclass(frozen=True) class Macro: - """Defines a cmd2 macro""" + """Defines a cmd2 macro.""" # Name of the macro name: str @@ -85,11 +78,11 @@ class Macro: minimum_arg_count: int # Used to fill in argument placeholders in the macro - arg_list: List[MacroArg] = field(default_factory=list) + arg_list: list[MacroArg] = field(default_factory=list) @dataclass(frozen=True) -class Statement(str): # type: ignore[override] +class Statement(str): # type: ignore[override] # noqa: SLOT000 """String subclass with additional attributes to store the results of parsing. The ``cmd`` module in the standard library passes commands around as a @@ -129,7 +122,7 @@ class Statement(str): # type: ignore[override] command: str = '' # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted - arg_list: List[str] = field(default_factory=list) + arg_list: list[str] = field(default_factory=list) # if the command is a multiline command, the name of the command, otherwise empty multiline_command: str = '' @@ -161,8 +154,7 @@ def __new__(cls, value: object, *pos_args: Any, **kw_args: Any) -> 'Statement': NOTE: @dataclass takes care of initializing other members in the __init__ it generates. """ - stmt = super().__new__(cls, value) - return stmt + return super().__new__(cls, value) @property def command_and_args(self) -> str: @@ -182,7 +174,7 @@ def command_and_args(self) -> str: @property def post_command(self) -> str: - """A string containing any ending terminator, suffix, and redirection chars""" + """A string containing any ending terminator, suffix, and redirection chars.""" rtn = '' if self.terminator: rtn += self.terminator @@ -202,12 +194,12 @@ def post_command(self) -> str: @property def expanded_command_line(self) -> str: - """Concatenate [command_and_args][cmd2.Statement.command_and_args] and [post_command][cmd2.Statement.post_command]""" + """Concatenate [command_and_args][cmd2.Statement.command_and_args] and [post_command][cmd2.Statement.post_command].""" return self.command_and_args + self.post_command @property - def argv(self) -> List[str]: - """a list of arguments a-la ``sys.argv``. + def argv(self) -> list[str]: + """A list of arguments a-la ``sys.argv``. The first element of the list is the command after shortcut and macro expansion. Subsequent elements of the list contain any additional @@ -218,21 +210,19 @@ def argv(self) -> List[str]: """ if self.command: rtn = [utils.strip_quotes(self.command)] - for cur_token in self.arg_list: - rtn.append(utils.strip_quotes(cur_token)) + rtn.extend(utils.strip_quotes(cur_token) for cur_token in self.arg_list) else: rtn = [] return rtn - def to_dict(self) -> Dict[str, Any]: - """Utility method to convert this Statement into a dictionary for use in persistent JSON history files""" + def to_dict(self) -> dict[str, Any]: + """Utility method to convert this Statement into a dictionary for use in persistent JSON history files.""" return self.__dict__.copy() @staticmethod - def from_dict(source_dict: Dict[str, Any]) -> 'Statement': - """ - Utility method to restore a Statement from a dictionary + def from_dict(source_dict: dict[str, Any]) -> 'Statement': + """Utility method to restore a Statement from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) :return: Statement object @@ -258,8 +248,8 @@ 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, + aliases: Optional[dict[str, str]] = None, + shortcuts: Optional[dict[str, str]] = None, ) -> None: """Initialize an instance of StatementParser. @@ -271,13 +261,13 @@ def __init__( :param aliases: dictionary containing aliases :param shortcuts: dictionary containing shortcuts """ - self.terminators: Tuple[str, ...] + self.terminators: tuple[str, ...] if terminators is None: self.terminators = (constants.MULTILINE_TERMINATOR,) else: self.terminators = tuple(terminators) - self.multiline_commands: Tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () - self.aliases: Dict[str, str] = aliases if aliases is not None else {} + self.multiline_commands: tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () + self.aliases: dict[str, str] = aliases if aliases is not None else {} if shortcuts is None: shortcuts = constants.DEFAULT_SHORTCUTS @@ -318,7 +308,7 @@ def __init__( expr = rf'\A\s*(\S*?)({second_group})' self._command_pattern = re.compile(expr) - def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[bool, str]: + def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[bool, str]: """Determine whether a word is a valid name for a command. Commands cannot include redirection characters, whitespace, @@ -340,7 +330,7 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[b valid = False if not isinstance(word, str): - return False, f'must be a string. Received {str(type(word))} instead' # type: ignore[unreachable] + return False, f'must be a string. Received {type(word)!s} instead' # type: ignore[unreachable] if not word: return False, 'cannot be an empty string' @@ -363,22 +353,19 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[b errmsg += ', '.join([shlex.quote(x) for x in errchars]) match = self._command_pattern.search(word) - if match: - if word == match.group(1): - valid = True - errmsg = '' + if match and word == match.group(1): + valid = True + errmsg = '' return valid, errmsg - def tokenize(self, line: str) -> List[str]: - """ - Lex a string into a list of tokens. Shortcuts and aliases are expanded and + def tokenize(self, line: str) -> list[str]: + """Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed. :param line: the command line being lexed :return: A list of tokens :raises Cmd2ShlexError: if a shlex error occurs (e.g. No closing quotation) """ - # expand shortcuts and aliases line = self._expand(line) @@ -393,12 +380,10 @@ def tokenize(self, line: str) -> List[str]: raise Cmd2ShlexError(ex) # custom lexing - tokens = self.split_on_punctuation(tokens) - return tokens + return self.split_on_punctuation(tokens) def parse(self, line: str) -> Statement: - """ - Tokenize the input and parse it into a [cmd2.parsing.Statement][] object, + """Tokenize the input and parse it into a [cmd2.parsing.Statement][] object, stripping comments, expanding aliases and shortcuts, and extracting output redirection directives. @@ -406,7 +391,6 @@ def parse(self, line: str) -> Statement: :return: a new [cmd2.parsing.Statement][] object :raises Cmd2ShlexError: if a shlex error occurs (e.g. No closing quotation) """ - # handle the special case/hardcoded terminator of a blank line # we have to do this before we tokenize because tokenizing # destroys all unquoted whitespace in the input @@ -522,13 +506,10 @@ def parse(self, line: str) -> Statement: arg_list = tokens[1:] # set multiline - if command in self.multiline_commands: - multiline_command = command - else: - multiline_command = '' + multiline_command = command if command in self.multiline_commands else '' # build the statement - statement = Statement( + return Statement( args, raw=line, command=command, @@ -540,7 +521,6 @@ def parse(self, line: str) -> Statement: output=output, output_to=output_to, ) - return statement def parse_command_only(self, rawinput: str) -> Statement: """Partially parse input into a [cmd2.Statement][] object. @@ -596,20 +576,15 @@ def parse_command_only(self, rawinput: str) -> Statement: args = '' # set multiline - if command in self.multiline_commands: - multiline_command = command - else: - multiline_command = '' + multiline_command = command if command in self.multiline_commands else '' # build the statement - statement = Statement(args, raw=rawinput, command=command, multiline_command=multiline_command) - return statement + return Statement(args, raw=rawinput, command=command, multiline_command=multiline_command) 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. + ) -> tuple[Statement, list[str]]: + """Convenience method used by the argument parsing decorators. Retrieves just the arguments being passed to their ``do_*`` methods as a list. @@ -636,12 +611,10 @@ def get_command_arg_list( if preserve_quotes: return to_parse, to_parse.arg_list - else: - return to_parse, to_parse.argv[1:] + return to_parse, to_parse.argv[1:] def _expand(self, line: str) -> str: - """Expand aliases and shortcuts""" - + """Expand aliases and shortcuts.""" # Make a copy of aliases so we can keep track of what aliases have been resolved to avoid an infinite loop remaining_aliases = list(self.aliases.keys()) keep_expanding = bool(remaining_aliases) @@ -667,16 +640,17 @@ def _expand(self, line: str) -> str: if line.startswith(shortcut): # If the next character after the shortcut isn't a space, then insert one shortcut_len = len(shortcut) + effective_expansion = expansion if len(line) == shortcut_len or line[shortcut_len] != ' ': - expansion += ' ' + effective_expansion += ' ' # Expand the shortcut - line = line.replace(shortcut, expansion, 1) + line = line.replace(shortcut, effective_expansion, 1) break return line @staticmethod - def _command_and_args(tokens: List[str]) -> Tuple[str, str]: + def _command_and_args(tokens: list[str]) -> tuple[str, str]: """Given a list of tokens, return a tuple of the command and the args as a string. """ @@ -691,7 +665,7 @@ def _command_and_args(tokens: List[str]) -> Tuple[str, str]: return command, args - def split_on_punctuation(self, tokens: List[str]) -> List[str]: + def split_on_punctuation(self, tokens: list[str]) -> list[str]: """Further splits tokens from a command line using punctuation characters. Punctuation characters are treated as word breaks when they are in @@ -701,7 +675,7 @@ def split_on_punctuation(self, tokens: List[str]) -> List[str]: :param tokens: the tokens as parsed by shlex :return: a new list of tokens, further split using punctuation """ - punctuation: List[str] = [] + punctuation: list[str] = [] punctuation.extend(self.terminators) punctuation.extend(constants.REDIRECTION_CHARS) diff --git a/cmd2/plugin.py b/cmd2/plugin.py index e7e2c6863..92cb80bd1 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,13 +1,9 @@ -# -# coding=utf-8 -"""Classes for the cmd2 plugin system""" +"""Classes for the cmd2 plugin system.""" from dataclasses import ( dataclass, ) -from typing import ( - Optional, -) +from typing import Optional from .parsing import ( Statement, @@ -16,7 +12,7 @@ @dataclass class PostparsingData: - """Data class containing information passed to postparsing hook methods""" + """Data class containing information passed to postparsing hook methods.""" stop: bool statement: Statement @@ -24,14 +20,14 @@ class PostparsingData: @dataclass class PrecommandData: - """Data class containing information passed to precommand hook methods""" + """Data class containing information passed to precommand hook methods.""" statement: Statement @dataclass class PostcommandData: - """Data class containing information passed to postcommand hook methods""" + """Data class containing information passed to postcommand hook methods.""" stop: bool statement: Statement @@ -39,7 +35,7 @@ class PostcommandData: @dataclass class CommandFinalizationData: - """Data class containing information passed to command finalization hook methods""" + """Data class containing information passed to command finalization hook methods.""" stop: bool statement: Optional[Statement] diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 2955cfa3d..1cccbaed4 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -1,6 +1,4 @@ -# coding=utf-8 -""" -Bridges calls made inside of a Python environment to the Cmd2 host app +"""Bridges calls made inside of a Python environment to the Cmd2 host app while maintaining a reasonable degree of isolation between the two. """ @@ -13,7 +11,6 @@ IO, TYPE_CHECKING, Any, - List, NamedTuple, Optional, TextIO, @@ -30,7 +27,7 @@ class CommandResult(NamedTuple): - """Encapsulates the results from a cmd2 app command + """Encapsulates the results from a cmd2 app command. :stdout: str - output captured from stdout while this command is executing :stderr: str - output captured from stderr while this command is executing @@ -71,20 +68,17 @@ class CommandResult(NamedTuple): data: Any = None def __bool__(self) -> bool: - """Returns True if the command succeeded, otherwise False""" - + """Returns True if the command succeeded, otherwise False.""" # If data was set, then use it to determine success if self.data is not None: return bool(self.data) # Otherwise check if stderr was filled out - else: - return not self.stderr + return not self.stderr class PyBridge: - """ - Provides a Python API wrapper for application commands. + """Provides a Python API wrapper for application commands. :param cmd2_app: app being controlled by this PyBridge. :param add_to_history: If True, then add all commands run by this PyBridge to history. @@ -99,19 +93,18 @@ def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None # Tells if any of the commands run via __call__ returned True for stop self.stop = False - def __dir__(self) -> List[str]: - """Return a custom set of attribute names""" - attributes: List[str] = [] + def __dir__(self) -> list[str]: + """Return a custom set of attribute names.""" + attributes: list[str] = [] attributes.insert(0, 'cmd_echo') return attributes def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResult: - """ - Provide functionality to call application commands by calling PyBridge + """Provide functionality to call application commands by calling PyBridge ex: app('help') :param command: command line being run :param echo: If provided, this temporarily overrides the value of self.cmd_echo while the - command runs. If True, output will be echoed to stdout/stderr. (Defaults to None) + command runs. If True, output will be echoed to stdout/stderr. (Defaults to None). """ if echo is None: @@ -131,23 +124,21 @@ def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResul stop = False try: self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout) - with redirect_stdout(cast(IO[str], copy_cmd_stdout)): - with redirect_stderr(cast(IO[str], copy_stderr)): - stop = self._cmd2_app.onecmd_plus_hooks( - command, - add_to_history=self._add_to_history, - py_bridge_call=True, - ) + with redirect_stdout(cast(IO[str], copy_cmd_stdout)), redirect_stderr(cast(IO[str], copy_stderr)): + stop = self._cmd2_app.onecmd_plus_hooks( + command, + add_to_history=self._add_to_history, + py_bridge_call=True, + ) finally: with self._cmd2_app.sigint_protection: self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream) self.stop = stop or self.stop # Save the result - result = CommandResult( + return CommandResult( stdout=copy_cmd_stdout.getvalue(), stderr=copy_stderr.getvalue(), stop=stop, data=self._cmd2_app.last_result, ) - return result diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index b02aa73a2..e765f28ac 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -1,15 +1,10 @@ -# coding=utf-8 -""" -Imports the proper Readline for the platform and provides utility functions for it -""" +"""Imports the proper Readline for the platform and provides utility functions for it.""" import sys from enum import ( Enum, ) -from typing import ( - Union, -) +from typing import Union ######################################################################################################################### # NOTE ON LIBEDIT: @@ -39,7 +34,7 @@ class RlType(Enum): - """Readline library types we support""" + """Readline library types we support.""" GNU = 1 PYREADLINE = 2 @@ -72,30 +67,29 @@ class RlType(Enum): if sys.stdout is not None and sys.stdout.isatty(): # pragma: no cover def enable_win_vt100(handle: HANDLE) -> bool: - """ - Enables VT100 character sequences in a Windows console + """Enables VT100 character sequences in a Windows console This only works on Windows 10 and up :param handle: the handle on which to enable vt100 - :return: True if vt100 characters are enabled for the handle + :return: True if vt100 characters are enabled for the handle. """ - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + enable_virtual_terminal_processing = 0x0004 # Get the current mode for this handle in the console cur_mode = DWORD(0) readline.rl.console.GetConsoleMode(handle, byref(cur_mode)) - retVal = False + ret_val = False # Check if ENABLE_VIRTUAL_TERMINAL_PROCESSING is already enabled - if (cur_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0: - retVal = True + if (cur_mode.value & enable_virtual_terminal_processing) != 0: + ret_val = True - elif readline.rl.console.SetConsoleMode(handle, cur_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING): + elif readline.rl.console.SetConsoleMode(handle, cur_mode.value | enable_virtual_terminal_processing): # Restore the original mode when we exit atexit.register(readline.rl.console.SetConsoleMode, handle, cur_mode) - retVal = True + ret_val = True - return retVal + return ret_val # Enable VT100 sequences for stdout and stderr STD_OUT_HANDLE = -11 @@ -107,15 +101,14 @@ def enable_win_vt100(handle: HANDLE) -> bool: ############################################################################################################ # pyreadline3 is incomplete in terms of the Python readline API. Add the missing functions we need. ############################################################################################################ - # readline.remove_history_item() + # Add missing `readline.remove_history_item()` try: getattr(readline, 'remove_history_item') except AttributeError: def pyreadline_remove_history_item(pos: int) -> None: - """ - An implementation of remove_history_item() for pyreadline3 - :param pos: The 0-based position in history to remove + """An implementation of remove_history_item() for pyreadline3 + :param pos: The 0-based position in history to remove. """ # Save of the current location of the history cursor saved_cursor = readline.rl.mode._history.history_cursor @@ -159,10 +152,9 @@ def pyreadline_remove_history_item(pos: int) -> None: def rl_force_redisplay() -> None: # pragma: no cover - """ - Causes readline to display the prompt and input text wherever the cursor is and start + """Causes readline to display the prompt and input text wherever the cursor is and start reading input from this location. This is the proper way to restore the input line after - printing to the screen + printing to the screen. """ if not sys.stdout.isatty(): return @@ -181,34 +173,25 @@ def rl_force_redisplay() -> None: # pragma: no cover def rl_get_point() -> int: # pragma: no cover - """ - Returns the offset of the current cursor position in rl_line_buffer - """ + """Returns the offset of the current cursor position in rl_line_buffer.""" if rl_type == RlType.GNU: return ctypes.c_int.in_dll(readline_lib, "rl_point").value - elif rl_type == RlType.PYREADLINE: + if rl_type == RlType.PYREADLINE: return int(readline.rl.mode.l_buffer.point) - else: - return 0 + return 0 def rl_get_prompt() -> str: # pragma: no cover - """Get Readline's prompt""" + """Get Readline's prompt.""" if rl_type == RlType.GNU: encoded_prompt = ctypes.c_char_p.in_dll(readline_lib, "rl_prompt").value - if encoded_prompt is None: - prompt = '' - else: - prompt = encoded_prompt.decode(encoding='utf-8') + prompt = '' if encoded_prompt is None else encoded_prompt.decode(encoding='utf-8') elif rl_type == RlType.PYREADLINE: prompt_data: Union[str, bytes] = readline.rl.prompt - if isinstance(prompt_data, bytes): - prompt = prompt_data.decode(encoding='utf-8') - else: - prompt = prompt_data + prompt = prompt_data.decode(encoding='utf-8') if isinstance(prompt_data, bytes) else prompt_data else: prompt = '' @@ -217,27 +200,21 @@ def rl_get_prompt() -> str: # pragma: no cover def rl_get_display_prompt() -> str: # pragma: no cover - """ - Get Readline's currently displayed prompt. + """Get Readline's currently displayed prompt. In GNU Readline, the displayed prompt sometimes differs from the prompt. This occurs in functions that use the prompt string as a message area, such as incremental search. """ if rl_type == RlType.GNU: encoded_prompt = ctypes.c_char_p.in_dll(readline_lib, "rl_display_prompt").value - if encoded_prompt is None: - prompt = '' - else: - prompt = encoded_prompt.decode(encoding='utf-8') + prompt = '' if encoded_prompt is None else encoded_prompt.decode(encoding='utf-8') return rl_unescape_prompt(prompt) - else: - return rl_get_prompt() + return rl_get_prompt() def rl_set_prompt(prompt: str) -> None: # pragma: no cover - """ - Sets Readline's prompt - :param prompt: the new prompt value + """Sets Readline's prompt + :param prompt: the new prompt value. """ escaped_prompt = rl_escape_prompt(prompt) @@ -250,8 +227,7 @@ def rl_set_prompt(prompt: str) -> None: # pragma: no cover def rl_escape_prompt(prompt: str) -> str: - """ - Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes + """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes. :param prompt: original prompt :return: prompt safe to pass to GNU Readline @@ -278,12 +254,11 @@ def rl_escape_prompt(prompt: str) -> str: return result - else: - return prompt + return prompt def rl_unescape_prompt(prompt: str) -> str: - """Remove escape characters from a Readline prompt""" + """Remove escape characters from a Readline prompt.""" if rl_type == RlType.GNU: escape_start = "\x01" escape_end = "\x02" @@ -293,16 +268,16 @@ def rl_unescape_prompt(prompt: str) -> str: def rl_in_search_mode() -> bool: # pragma: no cover - """Check if readline is doing either an incremental (e.g. Ctrl-r) or non-incremental (e.g. Esc-p) search""" + """Check if readline is doing either an incremental (e.g. Ctrl-r) or non-incremental (e.g. Esc-p) search.""" if rl_type == RlType.GNU: # GNU Readline defines constants that we can use to determine if in search mode. # RL_STATE_ISEARCH 0x0000080 # RL_STATE_NSEARCH 0x0000100 - IN_SEARCH_MODE = 0x0000180 + in_search_mode = 0x0000180 readline_state = ctypes.c_int.in_dll(readline_lib, "rl_readline_state").value - return bool(IN_SEARCH_MODE & readline_state) - elif rl_type == RlType.PYREADLINE: + return bool(in_search_mode & readline_state) + if rl_type == RlType.PYREADLINE: from pyreadline3.modes.emacs import ( # type: ignore[import] EmacsMode, ) @@ -317,5 +292,4 @@ def rl_in_search_mode() -> bool: # pragma: no cover readline.rl.mode._process_non_incremental_search_keyevent, ) return readline.rl.mode.process_keyevent_queue[-1] in search_funcs - else: - return False + return False diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 285253c82..6f8d0d806 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -1,6 +1,4 @@ -# coding=utf-8 -""" -cmd2 table creation API +"""cmd2 table creation API This API is built upon two core classes: Column and TableCreator The general use case is to inherit from TableCreator to create a table class with custom formatting options. There are already implemented and ready-to-use examples of this below TableCreator's code. @@ -11,17 +9,13 @@ from collections import ( deque, ) +from collections.abc import Sequence from enum import ( Enum, ) from typing import ( Any, - Deque, - List, Optional, - Sequence, - Tuple, - Union, ) from wcwidth import ( # type: ignore[import] @@ -40,7 +34,7 @@ class HorizontalAlignment(Enum): - """Horizontal alignment of text in a cell""" + """Horizontal alignment of text in a cell.""" LEFT = 1 CENTER = 2 @@ -48,7 +42,7 @@ class HorizontalAlignment(Enum): class VerticalAlignment(Enum): - """Vertical alignment of text in a cell""" + """Vertical alignment of text in a cell.""" TOP = 1 MIDDLE = 2 @@ -56,7 +50,7 @@ class VerticalAlignment(Enum): class Column: - """Table column configuration""" + """Table column configuration.""" def __init__( self, @@ -69,10 +63,9 @@ def __init__( data_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, data_vert_align: VerticalAlignment = VerticalAlignment.TOP, style_data_text: bool = True, - max_data_lines: Union[int, float] = constants.INFINITY, + max_data_lines: float = constants.INFINITY, ) -> None: - """ - Column initializer + """Column initializer. :param header: label for column header :param width: display width of column. This does not account for any borders or padding which @@ -99,8 +92,7 @@ def __init__( if width is not None and width < 1: raise ValueError("Column width cannot be less than 1") - else: - self.width: int = width if width is not None else -1 + self.width: int = width if width is not None else -1 self.header_horiz_align = header_horiz_align self.header_vert_align = header_vert_align @@ -117,8 +109,7 @@ def __init__( class TableCreator: - """ - Base table creation class. This class handles ANSI style sequences and characters with display widths greater than 1 + """Base table creation class. This class handles ANSI style sequences and characters with display widths greater than 1 when performing width calculations. It was designed with the ability to build tables one row at a time. This helps when you have large data sets that you don't want to hold in memory or when you receive portions of the data set incrementally. @@ -132,8 +123,7 @@ class TableCreator: """ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None: - """ - TableCreator initializer + """TableCreator initializer. :param cols: column definitions for this table :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab, @@ -156,9 +146,8 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None: col.width = max(1, ansi.widest_line(col.header)) @staticmethod - def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> Tuple[str, int, int]: - """ - Used by _wrap_text() to wrap a long word over multiple lines + def _wrap_long_word(word: str, max_width: int, max_lines: float, is_last_word: bool) -> tuple[str, int, int]: + """Used by _wrap_text() to wrap a long word over multiple lines. :param word: word being wrapped :param max_width: maximum display width of a line @@ -220,9 +209,8 @@ def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_ return wrapped_buf.getvalue(), total_lines, cur_line_width @staticmethod - def _wrap_text(text: str, max_width: int, max_lines: Union[int, float]) -> str: - """ - Wrap text into lines with a display width no longer than max_width. This function breaks words on whitespace + def _wrap_text(text: str, max_width: int, max_lines: float) -> str: + """Wrap text into lines with a display width no longer than max_width. This function breaks words on whitespace boundaries. If a word is longer than the space remaining on a line, then it will start on a new line. ANSI escape sequences do not count toward the width of a line. @@ -231,14 +219,12 @@ def _wrap_text(text: str, max_width: int, max_lines: Union[int, float]) -> str: :param max_lines: maximum lines to wrap before ending the last line displayed with an ellipsis :return: wrapped text """ - # MyPy Issue #7057 documents regression requiring nonlocals to be defined earlier cur_line_width = 0 total_lines = 0 def add_word(word_to_add: str, is_last_word: bool) -> None: - """ - Called from loop to add a word to the wrapped text + """Called from loop to add a word to the wrapped text. :param word_to_add: the word being added :param is_last_word: True if this is the last word of the total text being wrapped @@ -382,9 +368,8 @@ def add_word(word_to_add: str, is_last_word: bool) -> None: return wrapped_buf.getvalue() - def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> Tuple[Deque[str], int]: - """ - Generate the lines of a table cell + def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> tuple[deque[str], int]: + """Generate the lines of a table cell. :param cell_data: data to be included in cell :param is_header: True if writing a header cell, otherwise writing a data cell. This determines whether to @@ -428,8 +413,7 @@ def generate_row( inter_cell: str = (2 * SPACE), post_line: str = EMPTY, ) -> str: - """ - Generate a header or data table row + """Generate a header or data table row. :param row_data: data with an entry for each column in the row :param is_header: True if writing a header cell, otherwise writing a data cell. This determines whether to @@ -450,11 +434,11 @@ def generate_row( """ class Cell: - """Inner class which represents a table cell""" + """Inner class which represents a table cell.""" def __init__(self) -> None: # Data in this cell split into individual lines - self.lines: Deque[str] = deque() + self.lines: deque[str] = deque() # Display width of this cell self.width = 0 @@ -482,7 +466,7 @@ def __init__(self) -> None: total_lines = 0 # Generate the cells for this row - cells = list() + cells = [] for col_index, col in enumerate(self.cols): cell = Cell() @@ -544,8 +528,7 @@ def __init__(self) -> None: # of tables. They can be used as-is or serve as inspiration for other custom table classes. ############################################################################################################ class SimpleTable(TableCreator): - """ - Implementation of TableCreator which generates a borderless table with an optional divider row after the header. + """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. """ @@ -559,8 +542,7 @@ def __init__( header_bg: Optional[ansi.BgColor] = None, data_bg: Optional[ansi.BgColor] = None, ) -> None: - """ - SimpleTable initializer + """SimpleTable initializer. :param cols: column definitions for this table :param column_spacing: how many spaces to place between columns. Defaults to 2. @@ -598,20 +580,18 @@ def __init__( self.data_bg = data_bg def apply_header_bg(self, value: Any) -> str: - """ - If defined, apply the header background color to header text + """If defined, apply the header background color to header text :param value: object whose text is to be colored - :return: formatted text + :return: formatted text. """ if self.header_bg is None: return str(value) return ansi.style(value, bg=self.header_bg) def apply_data_bg(self, value: Any) -> str: - """ - If defined, apply the data background color to data text + """If defined, apply the data background color to data text :param value: object whose text is to be colored - :return: formatted data string + :return: formatted data string. """ if self.data_bg is None: return str(value) @@ -619,8 +599,7 @@ def apply_data_bg(self, value: Any) -> str: @classmethod def base_width(cls, num_cols: int, *, column_spacing: int = 2) -> int: - """ - Utility method to calculate the display width required for a table before data is added to it. + """Utility method to calculate the display width required for a table before data is added to it. This is useful when determining how wide to make your columns to have a table be a specific width. :param num_cols: how many columns the table will have @@ -641,20 +620,20 @@ def base_width(cls, num_cols: int, *, column_spacing: int = 2) -> int: return ansi.style_aware_wcswidth(data_row) - data_width def total_width(self) -> int: - """Calculate the total display width of this table""" + """Calculate the total display width of this table.""" base_width = self.base_width(len(self.cols), column_spacing=self.column_spacing) data_width = sum(col.width for col in self.cols) return base_width + data_width def generate_header(self) -> str: - """Generate table header with an optional divider row""" + """Generate table header with an optional divider row.""" header_buf = io.StringIO() fill_char = self.apply_header_bg(SPACE) inter_cell = self.apply_header_bg(self.column_spacing * SPACE) # Apply background color to header text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for col in self.cols: if col.style_header_text: to_display.append(self.apply_header_bg(col.header)) @@ -673,15 +652,14 @@ def generate_header(self) -> str: return header_buf.getvalue() def generate_divider(self) -> str: - """Generate divider row""" + """Generate divider row.""" if self.divider_char is None: return '' return utils.align_left('', fill_char=self.divider_char, width=self.total_width()) def generate_data_row(self, row_data: Sequence[Any]) -> str: - """ - Generate a data row + """Generate a data row. :param row_data: data with an entry for each column in the row :return: data row string @@ -694,7 +672,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: inter_cell = self.apply_data_bg(self.column_spacing * SPACE) # Apply background color to data text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for index, col in enumerate(self.cols): if col.style_data_text: to_display.append(self.apply_data_bg(row_data[index])) @@ -704,8 +682,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: return self.generate_row(to_display, is_header=False, fill_char=fill_char, inter_cell=inter_cell) 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 + """Generate a table from a data set. :param table_data: Data with an entry for each data row of the table. Each entry should have data for each column in the row. @@ -740,8 +717,7 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: class BorderedTable(TableCreator): - """ - Implementation of TableCreator which generates a table with borders around the table and between rows. Borders + """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. """ @@ -757,8 +733,7 @@ def __init__( header_bg: Optional[ansi.BgColor] = None, data_bg: Optional[ansi.BgColor] = None, ) -> None: - """ - BorderedTable initializer + """BorderedTable initializer. :param cols: column definitions for this table :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab, @@ -788,30 +763,27 @@ def __init__( self.data_bg = data_bg def apply_border_color(self, value: Any) -> str: - """ - If defined, apply the border foreground and background colors + """If defined, apply the border foreground and background colors :param value: object whose text is to be colored - :return: formatted text + :return: formatted text. """ if self.border_fg is None and self.border_bg is None: return str(value) return ansi.style(value, fg=self.border_fg, bg=self.border_bg) def apply_header_bg(self, value: Any) -> str: - """ - If defined, apply the header background color to header text + """If defined, apply the header background color to header text :param value: object whose text is to be colored - :return: formatted text + :return: formatted text. """ if self.header_bg is None: return str(value) return ansi.style(value, bg=self.header_bg) def apply_data_bg(self, value: Any) -> str: - """ - If defined, apply the data background color to data text + """If defined, apply the data background color to data text :param value: object whose text is to be colored - :return: formatted data string + :return: formatted data string. """ if self.data_bg is None: return str(value) @@ -819,8 +791,7 @@ def apply_data_bg(self, value: Any) -> str: @classmethod def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int = 1) -> int: - """ - Utility method to calculate the display width required for a table before data is added to it. + """Utility method to calculate the display width required for a table before data is added to it. This is useful when determining how wide to make your columns to have a table be a specific width. :param num_cols: how many columns the table will have @@ -841,13 +812,13 @@ def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int return ansi.style_aware_wcswidth(data_row) - data_width def total_width(self) -> int: - """Calculate the total display width of this table""" + """Calculate the total display width of this table.""" base_width = self.base_width(len(self.cols), column_borders=self.column_borders, padding=self.padding) data_width = sum(col.width for col in self.cols) return base_width + data_width def generate_table_top_border(self) -> str: - """Generate a border which appears at the top of the header and data section""" + """Generate a border which appears at the top of the header and data section.""" fill_char = '═' pre_line = '╔' + self.padding * '═' @@ -869,7 +840,7 @@ def generate_table_top_border(self) -> str: ) def generate_header_bottom_border(self) -> str: - """Generate a border which appears at the bottom of the header""" + """Generate a border which appears at the bottom of the header.""" fill_char = '═' pre_line = '╠' + self.padding * '═' @@ -891,7 +862,7 @@ def generate_header_bottom_border(self) -> str: ) def generate_row_bottom_border(self) -> str: - """Generate a border which appears at the bottom of rows""" + """Generate a border which appears at the bottom of rows.""" fill_char = '─' pre_line = '╟' + self.padding * '─' @@ -900,7 +871,6 @@ def generate_row_bottom_border(self) -> str: if self.column_borders: inter_cell += '┼' inter_cell += self.padding * '─' - inter_cell = inter_cell post_line = self.padding * '─' + '╢' @@ -914,7 +884,7 @@ def generate_row_bottom_border(self) -> str: ) def generate_table_bottom_border(self) -> str: - """Generate a border which appears at the bottom of the table""" + """Generate a border which appears at the bottom of the table.""" fill_char = '═' pre_line = '╚' + self.padding * '═' @@ -936,7 +906,7 @@ def generate_table_bottom_border(self) -> str: ) def generate_header(self) -> str: - """Generate table header""" + """Generate table header.""" fill_char = self.apply_header_bg(SPACE) pre_line = self.apply_border_color('║') + self.apply_header_bg(self.padding * SPACE) @@ -949,7 +919,7 @@ def generate_header(self) -> str: post_line = self.apply_header_bg(self.padding * SPACE) + self.apply_border_color('║') # Apply background color to header text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for col in self.cols: if col.style_header_text: to_display.append(self.apply_header_bg(col.header)) @@ -971,8 +941,7 @@ def generate_header(self) -> str: return header_buf.getvalue() def generate_data_row(self, row_data: Sequence[Any]) -> str: - """ - Generate a data row + """Generate a data row. :param row_data: data with an entry for each column in the row :return: data row string @@ -993,7 +962,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: post_line = self.apply_data_bg(self.padding * SPACE) + self.apply_border_color('║') # Apply background color to data text in Columns which allow it - to_display: List[Any] = [] + to_display: list[Any] = [] for index, col in enumerate(self.cols): if col.style_data_text: to_display.append(self.apply_data_bg(row_data[index])) @@ -1005,8 +974,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: ) def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True) -> str: - """ - Generate a table from a data set + """Generate a table from a data set. :param table_data: Data with an entry for each data row of the table. Each entry should have data for each column in the row. @@ -1038,8 +1006,7 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: class AlternatingTable(BorderedTable): - """ - Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border + """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. To nest an AlternatingTable within another AlternatingTable, set style_data_text to False on the Column @@ -1060,8 +1027,7 @@ def __init__( odd_bg: Optional[ansi.BgColor] = None, even_bg: Optional[ansi.BgColor] = ansi.Bg.DARK_GRAY, ) -> None: - """ - AlternatingTable initializer + """AlternatingTable initializer. Note: Specify background colors using subclasses of BgColor (e.g. Bg, EightBitBg, RgbBg) @@ -1094,21 +1060,18 @@ def __init__( self.even_bg = even_bg def apply_data_bg(self, value: Any) -> str: - """ - Apply background color to data text based on what row is being generated and whether a color has been defined + """Apply background color to data text based on what row is being generated and whether a color has been defined :param value: object whose text is to be colored - :return: formatted data string + :return: formatted data string. """ if self.row_num % 2 == 0 and self.even_bg is not None: return ansi.style(value, bg=self.even_bg) - elif self.row_num % 2 != 0 and self.odd_bg is not None: + if self.row_num % 2 != 0 and self.odd_bg is not None: return ansi.style(value, bg=self.odd_bg) - else: - return str(value) + return str(value) def generate_data_row(self, row_data: Sequence[Any]) -> str: - """ - Generate a data row + """Generate a data row. :param row_data: data with an entry for each column in the row :return: data row string @@ -1118,8 +1081,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str: return row def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True) -> str: - """ - Generate a table from a data set + """Generate a table from a data set. :param table_data: Data with an entry for each data row of the table. Each entry should have data for each column in the row. diff --git a/cmd2/transcript.py b/cmd2/transcript.py index f4781fd99..73e65f988 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -1,5 +1,3 @@ -# -# -*- coding: utf-8 -*- """Machinery for running and validating transcripts. If the user wants to run a transcript (see docs/transcript.rst), @@ -12,13 +10,11 @@ class is used in cmd2.py::run_transcript_tests() import re import unittest +from collections.abc import Iterator from typing import ( TYPE_CHECKING, - Iterator, - List, Optional, TextIO, - Tuple, cast, ) @@ -47,7 +43,7 @@ class Cmd2TestCase(unittest.TestCase): def setUp(self) -> None: if self.cmdapp: - self._fetchTranscripts() + self._fetch_transcripts() # Trap stdout self._orig_stdout = self.cmdapp.stdout @@ -58,15 +54,15 @@ def tearDown(self) -> None: # Restore stdout self.cmdapp.stdout = self._orig_stdout - def runTest(self) -> None: # was testall + def runTest(self) -> None: # was testall # noqa: N802 if self.cmdapp: its = sorted(self.transcripts.items()) for fname, transcript in its: self._test_transcript(fname, transcript) - def _fetchTranscripts(self) -> None: + def _fetch_transcripts(self) -> None: self.transcripts = {} - testfiles = cast(List[str], getattr(self.cmdapp, 'testfiles', [])) + testfiles = cast(list[str], getattr(self.cmdapp, 'testfiles', [])) for fname in testfiles: tfile = open(fname) self.transcripts[fname] = iter(tfile.readlines()) @@ -112,9 +108,9 @@ def _test_transcript(self, fname: str, transcript: Iterator[str]) -> None: # Read the expected result from transcript if ansi.strip_style(line).startswith(self.cmdapp.visible_prompt): message = f'\nFile {fname}, line {line_num}\nCommand was:\n{command}\nExpected: (nothing)\nGot:\n{result}\n' - self.assertTrue(not (result.strip()), message) + assert not result.strip(), message # noqa: S101 # If the command signaled the application to quit there should be no more commands - self.assertFalse(stop, stop_msg) + assert not stop, stop_msg # noqa: S101 continue expected_parts = [] while not ansi.strip_style(line).startswith(self.cmdapp.visible_prompt): @@ -128,13 +124,13 @@ def _test_transcript(self, fname: str, transcript: Iterator[str]) -> None: if stop: # This should only be hit if the command that set stop to True had output text - self.assertTrue(finished, stop_msg) + assert finished, stop_msg # noqa: S101 # transform the expected text into a valid regular expression expected = ''.join(expected_parts) expected = self._transform_transcript_expected(expected) message = f'\nFile {fname}, line {line_num}\nCommand was:\n{command}\nExpected:\n{expected}\nGot:\n{result}\n' - self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message) + assert re.match(expected, result, re.MULTILINE | re.DOTALL), message # noqa: S101 def _transform_transcript_expected(self, s: str) -> str: r"""Parse the string with slashed regexes into a valid regex. @@ -162,29 +158,28 @@ def _transform_transcript_expected(self, s: str) -> str: # no more slashes, add the rest of the string and bail regex += re.escape(s[start:]) break + # there is a slash, add everything we have found so far + # add stuff before the first slash as plain text + regex += re.escape(s[start:first_slash_pos]) + start = first_slash_pos + 1 + # and go find the next one + (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True) + if second_slash_pos > 0: + # add everything between the slashes (but not the slashes) + # as a regular expression + regex += s[start:second_slash_pos] + # and change where we start looking for slashed on the + # turn through the loop + start = second_slash_pos + 1 else: - # there is a slash, add everything we have found so far - # add stuff before the first slash as plain text - regex += re.escape(s[start:first_slash_pos]) - start = first_slash_pos + 1 - # and go find the next one - (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True) - if second_slash_pos > 0: - # add everything between the slashes (but not the slashes) - # as a regular expression - regex += s[start:second_slash_pos] - # and change where we start looking for slashed on the - # turn through the loop - start = second_slash_pos + 1 - else: - # No closing slash, we have to add the first slash, - # and the rest of the text - regex += re.escape(s[start - 1 :]) - break + # No closing slash, we have to add the first slash, + # and the rest of the text + regex += re.escape(s[start - 1 :]) + break return regex @staticmethod - def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str, int, int]: + def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> tuple[str, int, int]: """Find the next slash in {s} after {start} that is not preceded by a backslash. If we find an escaped slash, add everything up to and including it to regex, @@ -200,32 +195,31 @@ def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str, if pos == -1: # no match, return to caller break - elif pos == 0: + if pos == 0: # slash at the beginning of the string, so it can't be # escaped. We found it. break - else: - # check if the slash is preceded by a backslash - 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] - # 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]) - # and then add the slash as escaped - # plain text - regex += re.escape(s[pos]) - # update start to show we have handled everything - # before it - start = pos + 1 - # and continue to look + # check if the slash is preceded by a backslash + 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] + # skip the backslash, and add the slash + regex += s[pos] else: - # slash is not escaped, this is what we are looking for - break + # add everything up to the backslash as escaped + # plain text + regex += re.escape(s[start : pos - 1]) + # and then add the slash as escaped + # plain text + regex += re.escape(s[pos]) + # update start to show we have handled everything + # before it + start = pos + 1 + # and continue to look + else: + # slash is not escaped, this is what we are looking for + break return regex, pos, start diff --git a/cmd2/utils.py b/cmd2/utils.py index 2ad864171..d58a2b530 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1,5 +1,4 @@ -# coding=utf-8 -"""Shared utility functions""" +"""Shared utility functions.""" import argparse import collections @@ -13,6 +12,7 @@ import sys import threading import unicodedata +from collections.abc import Callable, Iterable from difflib import ( SequenceMatcher, ) @@ -22,13 +22,8 @@ from typing import ( TYPE_CHECKING, Any, - Callable, - Dict, - Iterable, - List, Optional, TextIO, - Type, TypeVar, Union, cast, @@ -53,8 +48,7 @@ def is_quoted(arg: str) -> bool: - """ - Checks if a string is quoted + """Checks if a string is quoted. :param arg: the string being checked for quotes :return: True if a string is quoted @@ -63,17 +57,14 @@ def is_quoted(arg: str) -> bool: def quote_string(arg: str) -> str: - """Quote a string""" - if '"' in arg: - quote = "'" - else: - quote = '"' + """Quote a string.""" + quote = "'" if '"' in arg else '"' return quote + arg + quote def quote_string_if_needed(arg: str) -> str: - """Quote a string if it contains spaces and isn't already quoted""" + """Quote a string if it contains spaces and isn't already quoted.""" if is_quoted(arg) or ' ' not in arg: return arg @@ -106,22 +97,21 @@ def to_bool(val: Any) -> bool: if isinstance(val, str): if val.capitalize() == str(True): return True - elif val.capitalize() == str(False): + if val.capitalize() == str(False): return False raise ValueError("must be True or False (case-insensitive)") - elif isinstance(val, bool): + if isinstance(val, bool): return val - else: - return bool(val) + return bool(val) class Settable: - """Used to configure an attribute to be settable via the set command in the CLI""" + """Used to configure an attribute to be settable via the set command in the CLI.""" def __init__( self, name: str, - val_type: Union[Type[Any], Callable[[Any], Any]], + val_type: Union[type[Any], Callable[[Any], Any]], description: str, settable_object: object, *, @@ -131,8 +121,7 @@ def __init__( choices_provider: Optional[ChoicesProviderFunc] = None, completer: Optional[CompleterFunc] = None, ) -> None: - """ - Settable Initializer + """Settable Initializer. :param name: name of the instance attribute being made settable :param val_type: callable used to cast the string value from the command line into its proper type and @@ -160,8 +149,8 @@ def __init__( """ if val_type is bool: - def get_bool_choices(_) -> List[str]: # type: ignore[no-untyped-def] - """Used to tab complete lowercase boolean values""" + def get_bool_choices(_) -> list[str]: # type: ignore[no-untyped-def] + """Used to tab complete lowercase boolean values.""" return ['true', 'false'] val_type = to_bool @@ -182,8 +171,7 @@ def get_value(self) -> Any: return getattr(self.settable_obj, self.settable_attrib_name) def set_value(self, value: Any) -> None: - """ - Set the settable attribute on the specified destination object. + """Set the settable attribute on the specified destination object. :param value: new value to set """ @@ -231,7 +219,7 @@ def is_text_file(file_path: str) -> bool: return valid_text_file -def remove_duplicates(list_to_prune: List[_T]) -> List[_T]: +def remove_duplicates(list_to_prune: list[_T]) -> list[_T]: """Removes duplicates from a list while preserving order of the items. :param list_to_prune: the list being pruned of duplicates @@ -253,7 +241,7 @@ def norm_fold(astr: str) -> str: return unicodedata.normalize('NFC', astr).casefold() -def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]: +def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]: """Sorts a list of strings alphabetically. For example: ['a1', 'A11', 'A2', 'a22', 'a3'] @@ -269,10 +257,9 @@ def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]: def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: - """ - Tries to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold. + """Tries to convert the passed-in string to an integer. If that fails, it converts it to lower case using norm_fold. :param input_str: string to convert - :return: the string as an integer or a lower case version of the string + :return: the string as an integer or a lower case version of the string. """ try: return int(input_str) @@ -280,9 +267,8 @@ def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: return norm_fold(input_str) -def natural_keys(input_str: str) -> List[Union[int, str]]: - """ - Converts a string into a list of integers and strings to support natural sorting (see natural_sort). +def natural_keys(input_str: str) -> list[Union[int, str]]: + """Converts a string into a list of integers and strings to support natural sorting (see natural_sort). For example: natural_keys('abc123def') -> ['abc', '123', 'def'] :param input_str: string to convert @@ -291,9 +277,8 @@ def natural_keys(input_str: str) -> List[Union[int, str]]: return [try_int_or_force_to_lower_case(substr) for substr in re.split(r'(\d+)', input_str)] -def natural_sort(list_to_sort: Iterable[str]) -> List[str]: - """ - Sorts a list of strings case insensitively as well as numerically. +def natural_sort(list_to_sort: Iterable[str]) -> list[str]: + """Sorts a list of strings case insensitively as well as numerically. For example: ['a1', 'A2', 'a3', 'A11', 'a22'] @@ -307,9 +292,8 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]: return sorted(list_to_sort, key=natural_keys) -def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None: - """ - Quote specific tokens in a list +def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None: + """Quote specific tokens in a list. :param tokens: token list being edited :param tokens_to_quote: the tokens, which if present in tokens, to quote @@ -319,9 +303,8 @@ def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None tokens[i] = quote_string(token) -def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> None: - """ - Unquote specific tokens in a list +def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> None: + """Unquote specific tokens in a list. :param tokens: token list being edited :param tokens_to_unquote: the tokens, which if present in tokens, to unquote @@ -333,9 +316,8 @@ def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> def expand_user(token: str) -> str: - """ - Wrap os.expanduser() to support expanding ~ in quoted strings - :param token: the string to expand + """Wrap os.expanduser() to support expanding ~ in quoted strings + :param token: the string to expand. """ if token: if is_quoted(token): @@ -353,20 +335,18 @@ def expand_user(token: str) -> str: return token -def expand_user_in_tokens(tokens: List[str]) -> None: - """ - Call expand_user() on all tokens in a list of strings - :param tokens: tokens to expand +def expand_user_in_tokens(tokens: list[str]) -> None: + """Call expand_user() on all tokens in a list of strings + :param tokens: tokens to expand. """ for index, _ in enumerate(tokens): tokens[index] = expand_user(tokens[index]) def find_editor() -> Optional[str]: - """ - Used to set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used. + """Used to set cmd2.Cmd.DEFAULT_EDITOR. If EDITOR env variable is set, that will be used. Otherwise the function will look for a known editor in directories specified by PATH env variable. - :return: Default editor or None + :return: Default editor or None. """ editor = os.environ.get('EDITOR') if not editor: @@ -377,17 +357,16 @@ def find_editor() -> Optional[str]: # Get a list of every directory in the PATH environment variable and ignore symbolic links env_path = os.getenv('PATH') - if env_path is None: - paths = [] - else: - paths = [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)] + paths = [] if env_path is None else [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)] - for editor, path in itertools.product(editors, paths): - editor_path = os.path.join(path, editor) + for possible_editor, path in itertools.product(editors, paths): + editor_path = os.path.join(path, possible_editor) if os.path.isfile(editor_path) and os.access(editor_path, os.X_OK): if sys.platform[:3] == 'win': # Remove extension from Windows file names - editor = os.path.splitext(editor)[0] + editor = os.path.splitext(possible_editor)[0] + else: + editor = possible_editor break else: editor = None @@ -395,7 +374,7 @@ def find_editor() -> Optional[str]: return editor -def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]: +def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> list[str]: """Return a list of file paths based on a glob pattern. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -407,7 +386,7 @@ def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]: return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)] -def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List[str]: +def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list[str]: """Return a list of file paths based on a list of glob patterns. Only files are returned, not directories, and optionally only files for which the user has a specified access to. @@ -423,8 +402,8 @@ def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List return files -def get_exes_in_path(starts_with: str) -> List[str]: - """Returns names of executables in a user's path +def get_exes_in_path(starts_with: str) -> list[str]: + """Returns names of executables in a user's path. :param starts_with: what the exes should start with. leave blank for all exes in path. :return: a list of matching exe names @@ -437,10 +416,7 @@ def get_exes_in_path(starts_with: str) -> List[str]: # Get a list of every directory in the PATH environment variable and ignore symbolic links env_path = os.getenv('PATH') - if env_path is None: - paths = [] - else: - paths = [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)] + paths = [] if env_path is None else [p for p in env_path.split(os.path.pathsep) if not os.path.islink(p)] # Use a set to store exe names since there can be duplicates exes_set = set() @@ -457,8 +433,7 @@ def get_exes_in_path(starts_with: str) -> List[str]: class StdSim: - """ - Class to simulate behavior of sys.stdout or sys.stderr. + """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. """ @@ -470,8 +445,7 @@ def __init__( encoding: str = 'utf-8', errors: str = 'replace', ) -> None: - """ - StdSim Initializer + """StdSim Initializer. :param inner_stream: the wrapped stream. Should be a TextIO or StdSim instance. :param echo: if True, then all input will be echoed to inner_stream @@ -486,8 +460,7 @@ def __init__( self.buffer = ByteBuf(self) def write(self, s: str) -> None: - """ - Add str to internal bytes buffer and if echo is True, echo contents to inner stream + """Add str to internal bytes buffer and if echo is True, echo contents to inner stream. :param s: String to write to the stream """ @@ -500,16 +473,15 @@ def write(self, s: str) -> None: self.inner_stream.write(s) def getvalue(self) -> str: - """Get the internal contents as a str""" + """Get the internal contents as a str.""" return self.buffer.byte_buf.decode(encoding=self.encoding, errors=self.errors) def getbytes(self) -> bytes: - """Get the internal contents as bytes""" + """Get the internal contents as bytes.""" return bytes(self.buffer.byte_buf) def read(self, size: Optional[int] = -1) -> str: - """ - Read from the internal contents as a str and then clear them out + """Read from the internal contents as a str and then clear them out. :param size: Number of bytes to read from the stream """ @@ -523,26 +495,24 @@ def read(self, size: Optional[int] = -1) -> str: return result def readbytes(self) -> bytes: - """Read from the internal contents as bytes and then clear them out""" + """Read from the internal contents as bytes and then clear them out.""" result = self.getbytes() self.clear() return result def clear(self) -> None: - """Clear the internal contents""" + """Clear the internal contents.""" self.buffer.byte_buf.clear() def isatty(self) -> bool: """StdSim only considered an interactive stream if `echo` is True and `inner_stream` is a tty.""" if self.echo: return self.inner_stream.isatty() - else: - return False + return False @property def line_buffering(self) -> bool: - """ - Handle when the inner stream doesn't have a line_buffering attribute which is the case + """Handle when the inner stream doesn't have a line_buffering attribute which is the case when running unit tests because pytest sets stdout to a pytest EncodedFile object. """ try: @@ -553,14 +523,11 @@ def line_buffering(self) -> bool: def __getattr__(self, item: str) -> Any: if item in self.__dict__: return self.__dict__[item] - else: - return getattr(self.inner_stream, item) + return getattr(self.inner_stream, item) class ByteBuf: - """ - Used by StdSim to write binary data and stores the actual bytes written - """ + """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'] @@ -582,23 +549,20 @@ def write(self, b: bytes) -> None: # and the bytes being written contain a new line character. This is helpful when StdSim # is being used to capture output of a shell command because it causes the output to print # to the screen more often than if we waited for the stream to flush its buffer. - if self.std_sim_instance.line_buffering: - if any(newline in b for newline in ByteBuf.NEWLINES): - self.std_sim_instance.flush() + if self.std_sim_instance.line_buffering and any(newline in b for newline in ByteBuf.NEWLINES): + self.std_sim_instance.flush() class ProcReader: - """ - Used to capture stdout and stderr from a Popen process if any of those were set to subprocess.PIPE. + """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: PopenTextIO, stdout: Union[StdSim, TextIO], stderr: Union[StdSim, TextIO]) -> None: - """ - ProcReader initializer + """ProcReader initializer :param proc: the Popen process being read from :param stdout: the stream to write captured stdout - :param stderr: the stream to write captured stderr + :param stderr: the stream to write captured stderr. """ self._proc = proc self._stdout = stdout @@ -615,7 +579,7 @@ def __init__(self, proc: PopenTextIO, stdout: Union[StdSim, TextIO], stderr: Uni self._err_thread.start() def send_sigint(self) -> None: - """Send a SIGINT to the process similar to if +C were pressed""" + """Send a SIGINT to the process similar to if +C were pressed.""" import signal if sys.platform.startswith('win'): @@ -632,11 +596,11 @@ def send_sigint(self) -> None: return def terminate(self) -> None: - """Terminate the process""" + """Terminate the process.""" self._proc.terminate() def wait(self) -> None: - """Wait for the process to finish""" + """Wait for the process to finish.""" if self._out_thread.is_alive(): self._out_thread.join() if self._err_thread.is_alive(): @@ -652,8 +616,7 @@ def wait(self) -> None: self._write_bytes(self._stderr, err) def _reader_thread_func(self, read_stdout: bool) -> None: - """ - Thread function that reads a stream from the process + """Thread function that reads a stream from the process :param read_stdout: if True, then this thread deals with stdout. Otherwise it deals with stderr. """ if read_stdout: @@ -664,7 +627,8 @@ def _reader_thread_func(self, read_stdout: bool) -> None: write_stream = self._stderr # The thread should have been started only if this stream was a pipe - assert read_stream is not None + if read_stream is None: + raise ValueError("read_stream is None") # Run until process completes while self._proc.poll() is None: @@ -675,10 +639,9 @@ def _reader_thread_func(self, read_stdout: bool) -> None: @staticmethod def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) -> None: - """ - Write bytes to a stream + """Write bytes to a stream :param stream: the stream being written to - :param to_write: the bytes being written + :param to_write: the bytes being written. """ if isinstance(to_write, str): to_write = to_write.encode() @@ -709,14 +672,14 @@ def __bool__(self) -> bool: def __enter__(self) -> None: self.__count += 1 - def __exit__(self, *args: Any) -> None: + def __exit__(self, *args: object) -> None: self.__count -= 1 if self.__count < 0: raise ValueError("count has gone below 0") class RedirectionSavedState: - """Created by each command to store information required to restore state after redirection""" + """Created by each command to store information required to restore state after redirection.""" def __init__( self, @@ -725,12 +688,11 @@ def __init__( pipe_proc_reader: Optional[ProcReader], saved_redirecting: bool, ) -> None: - """ - RedirectionSavedState initializer + """RedirectionSavedState initializer :param self_stdout: saved value of Cmd.stdout :param sys_stdout: saved value of sys.stdout :param pipe_proc_reader: saved value of Cmd._cur_pipe_proc_reader - :param saved_redirecting: saved value of Cmd._redirecting + :param saved_redirecting: saved value of Cmd._redirecting. """ # Tells if command is redirecting self.redirecting = False @@ -744,9 +706,8 @@ def __init__( self.saved_redirecting = saved_redirecting -def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]: - """ - Utility function for align_text() / truncate_line() which filters a style list down +def _remove_overridden_styles(styles_to_parse: list[str]) -> list[str]: + """Utility function for align_text() / truncate_line() which filters a style list down to only those which would still be in effect if all were processed in order. This is mainly used to reduce how many style strings are stored in memory when @@ -761,11 +722,11 @@ def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]: ) class StyleState: - """Keeps track of what text styles are enabled""" + """Keeps track of what text styles are enabled.""" def __init__(self) -> None: # Contains styles still in effect, keyed by their index in styles_to_parse - self.style_dict: Dict[int, str] = dict() + self.style_dict: dict[int, str] = {} # Indexes into style_dict self.reset_all: Optional[int] = None @@ -826,7 +787,7 @@ def __init__(self) -> None: class TextAlignment(Enum): - """Horizontal text alignment""" + """Horizontal text alignment.""" LEFT = 1 CENTER = 2 @@ -842,8 +803,7 @@ def align_text( tab_width: int = 4, truncate: bool = False, ) -> str: - """ - Align text for display within a given width. Supports characters with display widths greater than 1. + """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 independently. @@ -893,24 +853,21 @@ def align_text( # fill characters. Instead of repeating the style characters for each fill character, we'll wrap each sequence. fill_char_style_begin, fill_char_style_end = fill_char.split(stripped_fill_char) - if text: - lines = text.splitlines() - else: - lines = [''] + lines = text.splitlines() if text else [''] text_buf = io.StringIO() # ANSI style sequences that may affect subsequent lines will be cancelled by the fill_char's style. # To avoid this, we save styles which are still in effect so we can restore them when beginning the next line. # This also allows lines to be used independently and still have their style. TableCreator does this. - previous_styles: List[str] = [] + previous_styles: list[str] = [] for index, line in enumerate(lines): if index > 0: text_buf.write('\n') if truncate: - line = truncate_line(line, width) + line = truncate_line(line, width) # noqa: PLW2901 line_width = ansi.style_aware_wcswidth(line) if line_width == -1: @@ -969,8 +926,7 @@ def align_text( 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. + """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 independently. @@ -992,8 +948,7 @@ def align_left( 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. + """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 independently. @@ -1015,8 +970,7 @@ def align_center( 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. + """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 independently. @@ -1036,8 +990,7 @@ def align_right( def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: - """ - Truncate a single line to fit within a given display width. Any portion of the string that is truncated + """Truncate a single line to fit within a given display width. Any portion of the string that is truncated is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences do not count toward the display width. @@ -1114,9 +1067,8 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: return truncated_buf.getvalue() -def get_styles_dict(text: str) -> Dict[int, str]: - """ - Return an OrderedDict containing all ANSI style sequences found in a string +def get_styles_dict(text: str) -> dict[int, str]: + """Return an OrderedDict containing all ANSI style sequences found in a string. The structure of the dictionary is: key: index where sequences begins @@ -1153,7 +1105,6 @@ def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], ca :param category: category to put it in Example: - ```py import cmd2 class MyApp(cmd2.Cmd): @@ -1164,20 +1115,19 @@ def do_echo(self, arglist): ``` For an alternative approach to categorizing commands using a decorator, see [cmd2.decorators.with_category][] + """ if isinstance(func, Iterable): for item in func: setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category) + elif inspect.ismethod(func): + setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined] else: - if inspect.ismethod(func): - setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined] - else: - setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) + setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) -def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]: - """ - Attempts to resolve the class that defined a method. +def get_defining_class(meth: Callable[..., Any]) -> Optional[type[Any]]: + """Attempts to resolve the class that defined a method. Inspired by implementation published here: https://stackoverflow.com/a/25959545/1956611 @@ -1202,7 +1152,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]: class CompletionMode(Enum): - """Enum for what type of tab completion to perform in cmd2.Cmd.read_input()""" + """Enum for what type of tab completion to perform in cmd2.Cmd.read_input().""" # Tab completion will be disabled during read_input() call # Use of custom up-arrow history supported @@ -1220,11 +1170,10 @@ class CompletionMode(Enum): class CustomCompletionSettings: - """Used by cmd2.Cmd.complete() to tab complete strings other than command arguments""" + """Used by cmd2.Cmd.complete() to tab complete strings other than command arguments.""" def __init__(self, parser: argparse.ArgumentParser, *, preserve_quotes: bool = False) -> None: - """ - Initializer + """Initializer. :param parser: arg parser defining format of string being tab completed :param preserve_quotes: if True, then quoted tokens will keep their quotes when processed by @@ -1238,8 +1187,7 @@ def __init__(self, parser: argparse.ArgumentParser, *, preserve_quotes: bool = F def strip_doc_annotations(doc: str) -> str: - """ - Strip annotations from a docstring leaving only the text description + """Strip annotations from a docstring leaving only the text description. :param doc: documentation string """ @@ -1275,15 +1223,13 @@ def similarity_function(s1: str, s2: str) -> float: def suggest_similar( requested_command: str, options: Iterable[str], similarity_function_to_use: Optional[Callable[[str, str], float]] = None ) -> Optional[str]: - """ - Given a requested command and an iterable of possible options returns the most similar (if any is similar) + """Given a requested command and an iterable of possible options returns the most similar (if any is similar). :param requested_command: The command entered by the user :param options: The list of available commands to search for the most similar :param similarity_function_to_use: An optional callable to use to compare commands :return: The most similar command or None if no one is similar """ - proposed_command = None best_simil = MIN_SIMIL_TO_CONSIDER requested_command_to_compare = requested_command.lower() diff --git a/examples/alias_startup.py b/examples/alias_startup.py index 1ad493ffa..f6e401a0c 100755 --- a/examples/alias_startup.py +++ b/examples/alias_startup.py @@ -1,8 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating the following: 1) How to add custom command aliases using the alias command -2) How to run an initialization script at startup +2) How to run an initialization script at startup. """ import os @@ -13,13 +12,12 @@ class AliasAndStartup(cmd2.Cmd): """Example cmd2 application where we create commands that just print the arguments they are called with.""" - def __init__(self): + def __init__(self) -> None: alias_script = os.path.join(os.path.dirname(__file__), '.cmd2rc') super().__init__(startup_script=alias_script) - def do_nothing(self, args): + def do_nothing(self, args) -> None: """This command does nothing and produces no output.""" - pass if __name__ == '__main__': diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py index e42960b1c..5fe262d4c 100755 --- a/examples/arg_decorators.py +++ b/examples/arg_decorators.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# coding=utf-8 -"""An example demonstrating how use one of cmd2's argument parsing decorators""" +"""An example demonstrating how use one of cmd2's argument parsing decorators.""" import argparse import os @@ -9,7 +8,7 @@ class ArgparsingApp(cmd2.Cmd): - def __init__(self): + def __init__(self) -> None: super().__init__(include_ipy=True) self.intro = 'cmd2 has awesome decorators to make it easy to use Argparse to parse command arguments' @@ -21,13 +20,13 @@ def __init__(self): @cmd2.with_argparser(fsize_parser) def do_fsize(self, args: argparse.Namespace) -> None: - """Obtain the size of a file""" + """Obtain the size of a file.""" expanded_path = os.path.expanduser(args.file_path) try: size = os.path.getsize(expanded_path) except OSError as ex: - self.perror("Error retrieving size: {}".format(ex)) + self.perror(f"Error retrieving size: {ex}") return if args.unit == 'KB': @@ -39,8 +38,8 @@ def do_fsize(self, args: argparse.Namespace) -> None: size = round(size, 2) if args.comma: - size = '{:,}'.format(size) - self.poutput('{} {}'.format(size, args.unit)) + size = f'{size:,}' + self.poutput(f'{size} {args.unit}') # do_pow parser pow_parser = cmd2.Cmd2ArgumentParser() @@ -49,12 +48,11 @@ def do_fsize(self, args: argparse.Namespace) -> None: @cmd2.with_argparser(pow_parser) def do_pow(self, args: argparse.Namespace) -> None: - """ - Raise an integer to a small integer exponent, either positive or negative + """Raise an integer to a small integer exponent, either positive or negative. :param args: argparse arguments """ - self.poutput('{} ** {} == {}'.format(args.base, args.exponent, args.base**args.exponent)) + self.poutput(f'{args.base} ** {args.exponent} == {args.base**args.exponent}') if __name__ == '__main__': diff --git a/examples/arg_print.py b/examples/arg_print.py index 2cade9e42..506e92250 100755 --- a/examples/arg_print.py +++ b/examples/arg_print.py @@ -1,8 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating the following: 1) How arguments and options get parsed and passed to commands - 2) How to change what syntax gets parsed as a comment and stripped from the arguments + 2) How to change what syntax gets parsed as a comment and stripped from the arguments. This is intended to serve as a live demonstration so that developers can experiment with and understand how command and argument parsing work. @@ -16,28 +15,28 @@ class ArgumentAndOptionPrinter(cmd2.Cmd): """Example cmd2 application where we create commands that just print the arguments they are called with.""" - def __init__(self): + def __init__(self) -> None: # Create command shortcuts which are typically 1 character abbreviations which can be used in place of a command shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'$': 'aprint', '%': 'oprint'}) super().__init__(shortcuts=shortcuts) - def do_aprint(self, statement): + def do_aprint(self, statement) -> None: """Print the argument string this basic command is called with.""" - self.poutput('aprint was called with argument: {!r}'.format(statement)) - self.poutput('statement.raw = {!r}'.format(statement.raw)) - self.poutput('statement.argv = {!r}'.format(statement.argv)) - self.poutput('statement.command = {!r}'.format(statement.command)) + self.poutput(f'aprint was called with argument: {statement!r}') + self.poutput(f'statement.raw = {statement.raw!r}') + self.poutput(f'statement.argv = {statement.argv!r}') + self.poutput(f'statement.command = {statement.command!r}') @cmd2.with_argument_list - def do_lprint(self, arglist): + def do_lprint(self, arglist) -> None: """Print the argument list this basic command is called with.""" - self.poutput('lprint was called with the following list of arguments: {!r}'.format(arglist)) + self.poutput(f'lprint was called with the following list of arguments: {arglist!r}') @cmd2.with_argument_list(preserve_quotes=True) - def do_rprint(self, arglist): + def do_rprint(self, arglist) -> None: """Print the argument list this basic command is called with (with quotes preserved).""" - self.poutput('rprint was called with the following list of arguments: {!r}'.format(arglist)) + self.poutput(f'rprint was called with the following list of arguments: {arglist!r}') oprint_parser = cmd2.Cmd2ArgumentParser() oprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') @@ -46,9 +45,9 @@ def do_rprint(self, arglist): oprint_parser.add_argument('words', nargs='+', help='words to print') @cmd2.with_argparser(oprint_parser) - def do_oprint(self, args): + def do_oprint(self, args) -> None: """Print the options and argument list this options command was called with.""" - self.poutput('oprint was called with the following\n\toptions: {!r}'.format(args)) + self.poutput(f'oprint was called with the following\n\toptions: {args!r}') pprint_parser = cmd2.Cmd2ArgumentParser() pprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') @@ -56,9 +55,9 @@ def do_oprint(self, args): pprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') @cmd2.with_argparser(pprint_parser, with_unknown_args=True) - def do_pprint(self, args, unknown): + def do_pprint(self, args, unknown) -> None: """Print the options and argument list this options command was called with.""" - self.poutput('oprint was called with the following\n\toptions: {!r}\n\targuments: {}'.format(args, unknown)) + self.poutput(f'oprint was called with the following\n\toptions: {args!r}\n\targuments: {unknown}') if __name__ == '__main__': diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index e0ad101b9..43cad367b 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -1,14 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A simple example demonstrating how to integrate tab completion with argparse-based commands. -""" +"""A simple example demonstrating how to integrate tab completion with argparse-based commands.""" import argparse -from typing import ( - Dict, - List, -) from cmd2 import ( Cmd, @@ -24,17 +17,16 @@ class ArgparseCompletion(Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - def choices_provider(self) -> List[str]: - """A choices provider is useful when the choice list is based on instance data of your application""" + def choices_provider(self) -> list[str]: + """A choices provider is useful when the choice list is based on instance data of your application.""" return self.sport_item_strs - def choices_completion_error(self) -> List[str]: - """ - CompletionErrors can be raised if an error occurs while tab completing. + def choices_completion_error(self) -> list[str]: + """CompletionErrors can be raised if an error occurs while tab completing. Example use cases - Reading a database to retrieve a tab completion data set failed @@ -44,16 +36,15 @@ def choices_completion_error(self) -> List[str]: return self.sport_item_strs raise CompletionError("debug must be true") - def choices_completion_item(self) -> List[CompletionItem]: + def choices_completion_item(self) -> list[CompletionItem]: """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" fancy_item = "These things can\ncontain newlines and\n" fancy_item += ansi.style("styled text!!", fg=ansi.Fg.LIGHT_YELLOW, underline=True) items = {1: "My item", 2: "Another item", 3: "Yet another item", 4: fancy_item} return [CompletionItem(item_id, description) for item_id, description in items.items()] - def choices_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]: - """ - If a choices or completer function/method takes a value called arg_tokens, then it will be + def choices_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]: + """If a choices or completer function/method takes a value called arg_tokens, then it will be passed a dictionary that maps the command line tokens up through the one being completed to their argparse argument name. All values of the arg_tokens dictionary are lists, even if a particular argument expects only 1 token. @@ -106,7 +97,7 @@ def choices_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]: @with_argparser(example_parser) def do_example(self, _: argparse.Namespace) -> None: - """The example command""" + """The example command.""" self.poutput("I do nothing") diff --git a/examples/async_printing.py b/examples/async_printing.py index e94ee89a0..5399c2f70 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -1,16 +1,11 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A simple example demonstrating an application that asynchronously prints alerts, updates the prompt -and changes the window title +"""A simple example demonstrating an application that asynchronously prints alerts, updates the prompt +and changes the window title. """ import random import threading import time -from typing import ( - List, -) import cmd2 from cmd2 import ( @@ -33,10 +28,10 @@ class AlerterApp(cmd2.Cmd): - """An app that shows off async_alert() and async_update_prompt()""" + """An app that shows off async_alert() and async_update_prompt().""" def __init__(self, *args, **kwargs) -> None: - """Initializer""" + """Initializer.""" super().__init__(*args, **kwargs) self.prompt = "(APR)> " @@ -52,7 +47,7 @@ def __init__(self, *args, **kwargs) -> None: self.register_postloop_hook(self._postloop_hook) def _preloop_hook(self) -> None: - """Start the alerter thread""" + """Start the alerter thread.""" # This runs after cmdloop() acquires self.terminal_lock, which will be locked until the prompt appears. # Therefore this is the best place to start the alerter thread since there is no risk of it alerting # before the prompt is displayed. You can also start it via a command if its not something that should @@ -63,8 +58,7 @@ def _preloop_hook(self) -> None: self._alerter_thread.start() def _postloop_hook(self) -> None: - """Stops the alerter thread""" - + """Stops the alerter thread.""" # After this function returns, cmdloop() releases self.terminal_lock which could make the alerter # thread think the prompt is on screen. Therefore this is the best place to stop the alerter thread. # You can also stop it via a command. See do_stop_alerts(). @@ -72,8 +66,8 @@ def _postloop_hook(self) -> None: if self._alerter_thread.is_alive(): self._alerter_thread.join() - def do_start_alerts(self, _): - """Starts the alerter thread""" + def do_start_alerts(self, _) -> None: + """Starts the alerter thread.""" if self._alerter_thread.is_alive(): print("The alert thread is already started") else: @@ -81,21 +75,18 @@ def do_start_alerts(self, _): self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func) self._alerter_thread.start() - def do_stop_alerts(self, _): - """Stops the alerter thread""" + def do_stop_alerts(self, _) -> None: + """Stops the alerter thread.""" self._stop_event.set() if self._alerter_thread.is_alive(): self._alerter_thread.join() else: print("The alert thread is already stopped") - def _get_alerts(self) -> List[str]: + def _get_alerts(self) -> list[str]: + """Reports alerts + :return: the list of alerts. """ - Reports alerts - :return: the list of alerts - """ - global ALERTS - cur_time = time.monotonic() if cur_time < self._next_alert_time: return [] @@ -112,21 +103,18 @@ def _get_alerts(self) -> List[str]: if rand_num > 2: return [] - for i in range(0, rand_num): + for i in range(rand_num): self._alert_count += 1 - alerts.append("Alert {}".format(self._alert_count)) + alerts.append(f"Alert {self._alert_count}") self._next_alert_time = 0 return alerts def _generate_alert_str(self) -> str: + """Combines alerts into one string that can be printed to the terminal + :return: the alert string. """ - Combines alerts into one string that can be printed to the terminal - :return: the alert string - """ - global ALERTS - alert_str = '' alerts = self._get_alerts() @@ -146,9 +134,8 @@ def _generate_alert_str(self) -> str: return alert_str def _generate_colored_prompt(self) -> str: - """ - Randomly generates a colored prompt - :return: the new prompt + """Randomly generates a colored prompt + :return: the new prompt. """ rand_num = random.randint(1, 20) @@ -168,8 +155,7 @@ def _generate_colored_prompt(self) -> str: return style(self.visible_prompt, fg=status_color) def _alerter_thread_func(self) -> None: - """Prints alerts and updates the prompt any time the prompt is showing""" - + """Prints alerts and updates the prompt any time the prompt is showing.""" self._alert_count = 0 self._next_alert_time = 0 @@ -187,7 +173,7 @@ def _alerter_thread_func(self) -> None: if alert_str: # new_prompt is an optional parameter to async_alert() self.async_alert(alert_str, new_prompt) - new_title = "Alerts Printed: {}".format(self._alert_count) + new_title = f"Alerts Printed: {self._alert_count}" self.set_window_title(new_title) # Otherwise check if the prompt needs to be updated or refreshed diff --git a/examples/basic.py b/examples/basic.py index 6ce4d2838..20ebe20a5 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,12 +1,11 @@ #!/usr/bin/env python3 -# coding=utf-8 """A simple example demonstrating the following: 1) How to add a command 2) How to add help for that command 3) Persistent history 4) How to run an initialization script at startup 5) How to add custom command aliases using the alias command -6) Shell-like capabilities +6) Shell-like capabilities. """ import cmd2 @@ -20,7 +19,7 @@ class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' - def __init__(self): + def __init__(self) -> None: super().__init__( multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', @@ -37,13 +36,13 @@ def __init__(self): self.default_category = 'cmd2 Built-in Commands' @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _): - """Display the intro banner""" + def do_intro(self, _) -> None: + """Display the intro banner.""" self.poutput(self.intro) @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg): - """Example of a multiline command""" + def do_echo(self, arg) -> None: + """Example of a multiline command.""" self.poutput(arg) diff --git a/examples/basic_completion.py b/examples/basic_completion.py index c713f2b0d..e1391540f 100755 --- a/examples/basic_completion.py +++ b/examples/basic_completion.py @@ -1,12 +1,10 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A simple example demonstrating how to enable tab completion by assigning a completer function to do_* commands. +"""A simple example demonstrating how to enable tab completion by assigning a completer function to do_* commands. This also demonstrates capabilities of the following completer features included with cmd2: - CompletionError exceptions - delimiter_complete() - flag_based_complete() (see note below) -- index_based_complete() (see note below) +- index_based_complete() (see note below). flag_based_complete() and index_based_complete() are basic methods and should only be used if you are not familiar with argparse. The recommended approach for tab completing positional tokens and flags is to use @@ -14,9 +12,6 @@ """ import functools -from typing import ( - List, -) import cmd2 @@ -35,19 +30,19 @@ class BasicCompletion(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_flag_based(self, statement: cmd2.Statement): + def do_flag_based(self, statement: cmd2.Statement) -> None: """Tab completes arguments based on a preceding flag using flag_based_complete -f, --food [completes food items] -s, --sport [completes sports] - -p, --path [completes local file system paths] + -p, --path [completes local file system paths]. """ - self.poutput("Args: {}".format(statement.args)) + self.poutput(f"Args: {statement.args}") - def complete_flag_based(self, text, line, begidx, endidx) -> List[str]: - """Completion function for do_flag_based""" + 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, @@ -62,12 +57,12 @@ def complete_flag_based(self, text, line, begidx, endidx) -> List[str]: return self.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) - def do_index_based(self, statement: cmd2.Statement): - """Tab completes first 3 arguments using index_based_complete""" - self.poutput("Args: {}".format(statement.args)) + def do_index_based(self, statement: cmd2.Statement) -> None: + """Tab completes first 3 arguments using index_based_complete.""" + self.poutput(f"Args: {statement.args}") - def complete_index_based(self, text, line, begidx, endidx) -> List[str]: - """Completion function for do_index_based""" + 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 @@ -76,20 +71,19 @@ def complete_index_based(self, text, line, begidx, endidx) -> List[str]: return self.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) - def do_delimiter_complete(self, statement: cmd2.Statement): - """Tab completes files from a list using delimiter_complete""" - self.poutput("Args: {}".format(statement.args)) + def do_delimiter_complete(self, statement: cmd2.Statement) -> None: + """Tab completes files from a list using delimiter_complete.""" + self.poutput(f"Args: {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='/') - def do_raise_error(self, statement: cmd2.Statement): - """Demonstrates effect of raising CompletionError""" - self.poutput("Args: {}".format(statement.args)) + def do_raise_error(self, statement: cmd2.Statement) -> None: + """Demonstrates effect of raising CompletionError.""" + self.poutput(f"Args: {statement.args}") - def complete_raise_error(self, text, line, begidx, endidx) -> List[str]: - """ - CompletionErrors can be raised if an error occurs while tab completing. + def complete_raise_error(self, text, line, begidx, endidx) -> list[str]: + """CompletionErrors can be raised if an error occurs while tab completing. Example use cases - Reading a database to retrieve a tab completion data set failed diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 75d53ed73..e12be178d 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2. +"""A sample application for cmd2. This example is very similar to example.py, but had additional code in main() that shows how to accept a command from @@ -22,12 +20,12 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application.""" # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist - # default_to_shell = True + # default_to_shell = True # noqa: ERA001 MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] - def __init__(self): + def __init__(self) -> None: shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell @@ -45,12 +43,12 @@ def __init__(self): speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) - def do_speak(self, args): + def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = f'{word[1:]}{word[0]}ay' if args.shout: word = word.upper() words.append(word) @@ -67,7 +65,7 @@ def do_speak(self, args): mumble_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(mumble_parser) - def do_mumble(self, args): + def do_mumble(self, args) -> None: """Mumbles what you tell me to.""" repetitions = args.repeat or 1 for i in range(min(repetitions, self.maxrepeats)): @@ -84,8 +82,7 @@ def do_mumble(self, args): def main(argv=None): - """Run when invoked from the operating system shell""" - + """Run when invoked from the operating system shell.""" parser = cmd2.Cmd2ArgumentParser(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) diff --git a/examples/colors.py b/examples/colors.py index 34f16da2c..fad3c9586 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2. Demonstrating colorized output. +"""A sample application for cmd2. Demonstrating colorized output. Experiment with the command line options on the `speak` command to see how different output colors ca @@ -37,7 +35,7 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" - def __init__(self): + def __init__(self) -> None: # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell super().__init__(include_ipy=True) @@ -59,12 +57,12 @@ def __init__(self): speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) - def do_speak(self, args): + def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = f'{word[1:]}{word[0]}ay' if args.shout: word = word.upper() words.append(word) @@ -79,8 +77,8 @@ def do_speak(self, args): # .poutput handles newlines, and accommodates output redirection too self.poutput(output_str) - def do_timetravel(self, _): - """A command which always generates an error message, to demonstrate custom error colors""" + def do_timetravel(self, _) -> None: + """A command which always generates an error message, to demonstrate custom error colors.""" self.perror('Mr. Fusion failed to start. Could not energize flux capacitor.') diff --git a/examples/custom_parser.py b/examples/custom_parser.py index 94df3b054..c814a2996 100644 --- a/examples/custom_parser.py +++ b/examples/custom_parser.py @@ -1,7 +1,4 @@ -# coding=utf-8 -""" -Defines the CustomParser used with override_parser.py example -""" +"""Defines the CustomParser used with override_parser.py example.""" import sys @@ -14,13 +11,13 @@ # First define the parser class CustomParser(Cmd2ArgumentParser): - """Overrides error class""" + """Overrides error class.""" def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def error(self, message: str) -> None: - """Custom override that applies custom formatting to the error message""" + """Custom override that applies custom formatting to the error message.""" lines = message.split('\n') linum = 0 formatted_message = '' @@ -35,7 +32,7 @@ def error(self, message: str) -> None: # Format errors with style_warning() formatted_message = ansi.style_warning(formatted_message) - self.exit(2, '{}\n\n'.format(formatted_message)) + self.exit(2, f'{formatted_message}\n\n') # Now set the default parser for a cmd2 app diff --git a/examples/decorator_example.py b/examples/decorator_example.py index ea8fd3b50..10b044713 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """A sample application showing how to use cmd2's argparse decorators to process command line arguments for your application. @@ -12,9 +11,6 @@ """ import argparse -from typing import ( - List, -) import cmd2 @@ -22,7 +18,7 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application.""" - def __init__(self, ip_addr=None, port=None, transcript_files=None): + def __init__(self, ip_addr=None, port=None, transcript_files=None) -> None: shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) super().__init__(transcript_files=transcript_files, multiline_commands=['orate'], shortcuts=shortcuts) @@ -36,7 +32,7 @@ def __init__(self, ip_addr=None, port=None, transcript_files=None): self._port = port # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist - # self.default_to_shell = True + # self.default_to_shell = True # noqa: ERA001 speak_parser = cmd2.Cmd2ArgumentParser() speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') @@ -45,12 +41,12 @@ def __init__(self, ip_addr=None, port=None, transcript_files=None): speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) - def do_speak(self, args: argparse.Namespace): + def do_speak(self, args: argparse.Namespace) -> None: """Repeats what you tell me to.""" words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = f'{word[1:]}{word[0]}ay' if args.shout: word = word.upper() words.append(word) @@ -66,18 +62,18 @@ def do_speak(self, args: argparse.Namespace): tag_parser.add_argument('content', nargs='+', help='content to surround with tag') @cmd2.with_argparser(tag_parser) - def do_tag(self, args: argparse.Namespace): - """create an html tag""" + def do_tag(self, args: argparse.Namespace) -> None: + """Create an html tag.""" # The Namespace always includes the Statement object created when parsing the command line statement = args.cmd2_statement.get() - self.poutput("The command line you ran was: {}".format(statement.command_and_args)) + self.poutput(f"The command line you ran was: {statement.command_and_args}") self.poutput("It generated this tag:") self.poutput('<{0}>{1}'.format(args.tag, ' '.join(args.content))) @cmd2.with_argument_list - def do_tagg(self, arglist: List[str]): - """version of creating an html tag using arglist instead of argparser""" + def do_tagg(self, arglist: list[str]) -> None: + """Version of creating an html tag using arglist instead of argparser.""" if len(arglist) >= 2: tag = arglist[0] content = arglist[1:] diff --git a/examples/default_categories.py b/examples/default_categories.py index 0fd485ae3..fd681a3c3 100755 --- a/examples/default_categories.py +++ b/examples/default_categories.py @@ -1,8 +1,5 @@ #!/usr/bin/env python3 -# coding=utf-8 -""" -Simple example demonstrating basic CommandSet usage. -""" +"""Simple example demonstrating basic CommandSet usage.""" import cmd2 from cmd2 import ( @@ -13,74 +10,64 @@ @with_default_category('Default Category') class MyBaseCommandSet(CommandSet): - """Defines a default category for all sub-class CommandSets""" - - pass + """Defines a default category for all sub-class CommandSets.""" class ChildInheritsParentCategories(MyBaseCommandSet): - """ - This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category' - """ + """This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category'.""" - def do_hello(self, _: cmd2.Statement): + def do_hello(self, _: cmd2.Statement) -> None: self._cmd.poutput('Hello') - def do_world(self, _: cmd2.Statement): + def do_world(self, _: cmd2.Statement) -> None: self._cmd.poutput('World') @with_default_category('Non-Heritable Category', heritable=False) 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' + """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): + def do_goodbye(self, _: cmd2.Statement) -> None: self._cmd.poutput('Goodbye') class GrandchildInheritsGrandparentCategory(ChildOverridesParentCategoriesNonHeritable): - """ - This subclass's parent class declared its default category non-heritable. Instead, it inherits the category defined + """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): + def do_aloha(self, _: cmd2.Statement) -> None: self._cmd.poutput('Aloha') @with_default_category('Heritable Category') class ChildOverridesParentCategories(MyBaseCommandSet): - """ - This subclass is decorated with a default category that is heritable. This overrides the parent class's default + """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): + def do_bonjour(self, _: cmd2.Statement) -> None: self._cmd.poutput('Bonjour') 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' + """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): + def do_monde(self, _: cmd2.Statement) -> None: self._cmd.poutput('Monde') class ExampleApp(cmd2.Cmd): - """ - Example to demonstrate heritable default categories - """ + """Example to demonstrate heritable default categories.""" - def __init__(self): - super(ExampleApp, self).__init__() + def __init__(self) -> None: + super().__init__() - def do_something(self, arg): + def do_something(self, arg) -> None: self.poutput('this is the something command') diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py index 82dde732d..f7740da79 100755 --- a/examples/dynamic_commands.py +++ b/examples/dynamic_commands.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# coding=utf-8 """A simple example demonstrating how do_* commands can be created in a loop.""" import functools @@ -17,7 +16,7 @@ class CommandsInLoop(cmd2.Cmd): """Example of dynamically adding do_* commands.""" - def __init__(self): + def __init__(self) -> None: # Add dynamic commands before calling cmd2.Cmd's init since it validates command names for command in COMMAND_LIST: # Create command function and add help category to it @@ -35,13 +34,13 @@ def __init__(self): super().__init__(include_ipy=True) - def send_text(self, args: cmd2.Statement, *, text: str): + def send_text(self, args: cmd2.Statement, *, text: str) -> None: """Simulate sending text to a server and printing the response.""" self.poutput(text.capitalize()) - def text_help(self, *, text: str): + def text_help(self, *, text: str) -> None: """Deal with printing help for the dynamically added commands.""" - self.poutput("Simulate sending {!r} to a server and printing the response".format(text)) + self.poutput(f"Simulate sending {text!r} to a server and printing the response") if __name__ == '__main__': diff --git a/examples/environment.py b/examples/environment.py index 1bb9812be..1983b3d21 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -1,8 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2 demonstrating customized environment parameters -""" +"""A sample application for cmd2 demonstrating customized environment parameters.""" import cmd2 @@ -10,7 +7,7 @@ class EnvironmentApp(cmd2.Cmd): """Example cmd2 application.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.degrees_c = 22 self.sunny = False @@ -19,17 +16,17 @@ def __init__(self): ) self.add_settable(cmd2.Settable('sunny', bool, 'Is it sunny outside?', self)) - def do_sunbathe(self, arg): + def do_sunbathe(self, arg) -> None: """Attempt to sunbathe.""" if self.degrees_c < 20: - result = "It's {} C - are you a penguin?".format(self.degrees_c) + result = f"It's {self.degrees_c} C - are you a penguin?" elif not self.sunny: result = 'Too dim.' else: result = 'UV is bad for your skin.' self.poutput(result) - def _onchange_degrees_c(self, param_name, old, new): + def _onchange_degrees_c(self, param_name, old, new) -> None: # if it's over 40C, it's gotta be sunny, right? if new > 40: self.sunny = True diff --git a/examples/event_loops.py b/examples/event_loops.py index e5435181a..aca434207 100755 --- a/examples/event_loops.py +++ b/examples/event_loops.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """A sample application for integrating cmd2 with external event loops. This is an example of how to use cmd2 in a way so that cmd2 doesn't own the inner event loop of your application. @@ -13,7 +12,7 @@ class Cmd2EventBased(cmd2.Cmd): """Basic example of how to run cmd2 without it controlling the main loop.""" - def __init__(self): + def __init__(self) -> None: super().__init__() # ... your class code here ... diff --git a/examples/example.py b/examples/example.py index 2ff64d747..fc083c513 100755 --- a/examples/example.py +++ b/examples/example.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2. +"""A sample application for cmd2. Thanks to cmd2's built-in transcript testing capability, it also serves as a test suite for example.py when used with the transcript_regex.txt transcript. @@ -20,12 +18,12 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application.""" # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist - # default_to_shell = True + # default_to_shell = True # noqa: ERA001 MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] - def __init__(self): + def __init__(self) -> None: shortcuts = cmd2.DEFAULT_SHORTCUTS shortcuts.update({'&': 'speak'}) super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) @@ -41,12 +39,12 @@ def __init__(self): speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) - def do_speak(self, args): + def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = f'{word[1:]}{word[0]}ay' if args.shout: word = word.upper() words.append(word) @@ -63,7 +61,7 @@ def do_speak(self, args): mumble_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(mumble_parser) - def do_mumble(self, args): + def do_mumble(self, args) -> None: """Mumbles what you tell me to.""" repetitions = args.repeat or 1 for _ in range(min(repetitions, self.maxrepeats)): diff --git a/examples/exit_code.py b/examples/exit_code.py index d8e538ced..bfce8c909 100755 --- a/examples/exit_code.py +++ b/examples/exit_code.py @@ -1,33 +1,29 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application.""" -from typing import ( - List, -) - import cmd2 class ReplWithExitCode(cmd2.Cmd): """Example cmd2 application where we can specify an exit code when existing.""" - def __init__(self): + def __init__(self) -> None: super().__init__() @cmd2.with_argument_list - def do_exit(self, arg_list: List[str]) -> bool: + def do_exit(self, arg_list: list[str]) -> bool: """Exit the application with an optional exit code. Usage: exit [exit_code] Where: - * exit_code - integer exit code to return to the shell""" + * exit_code - integer exit code to return to the shell + """ # If an argument was provided if arg_list: try: self.exit_code = int(arg_list[0]) except ValueError: - self.perror("{} isn't a valid integer exit code".format(arg_list[0])) + self.perror(f"{arg_list[0]} isn't a valid integer exit code") self.exit_code = 1 return True @@ -38,5 +34,5 @@ def do_exit(self, arg_list: List[str]) -> bool: app = ReplWithExitCode() sys_exit_code = app.cmdloop() - app.poutput('{!r} exiting with code: {}'.format(sys.argv[0], sys_exit_code)) + app.poutput(f'{sys.argv[0]!r} exiting with code: {sys_exit_code}') sys.exit(sys_exit_code) diff --git a/examples/first_app.py b/examples/first_app.py index 57a76f708..c82768a37 100755 --- a/examples/first_app.py +++ b/examples/first_app.py @@ -1,16 +1,14 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A simple application using cmd2 which demonstrates 8 key features: - - * Settings - * Commands - * Argument Parsing - * Generating Output - * Help - * Shortcuts - * Multiline Commands - * History +"""A simple application using cmd2 which demonstrates 8 key features: + +* Settings +* Commands +* Argument Parsing +* Generating Output +* Help +* Shortcuts +* Multiline Commands +* History """ import cmd2 @@ -19,7 +17,7 @@ class FirstApp(cmd2.Cmd): """A simple cmd2 application.""" - def __init__(self): + def __init__(self) -> None: shortcuts = cmd2.DEFAULT_SHORTCUTS shortcuts.update({'&': 'speak'}) super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) @@ -35,12 +33,12 @@ def __init__(self): speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) - def do_speak(self, args): + def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for word in args.words: if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + word = f'{word[1:]}{word[0]}ay' if args.shout: word = word.upper() words.append(word) diff --git a/examples/hello_cmd2.py b/examples/hello_cmd2.py index a67205834..a480aa5e4 100755 --- a/examples/hello_cmd2.py +++ b/examples/hello_cmd2.py @@ -1,8 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -This is intended to be a completely bare-bones cmd2 application suitable for rapid testing and debugging. -""" +"""This is intended to be a completely bare-bones cmd2 application suitable for rapid testing and debugging.""" from cmd2 import ( cmd2, diff --git a/examples/help_categories.py b/examples/help_categories.py index 5c349422c..923c16468 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for tagging categories on commands. +"""A sample application for tagging categories on commands. It also demonstrates the effects of decorator order when it comes to argparse errors occurring. """ @@ -34,27 +32,27 @@ class HelpCategories(cmd2.Cmd): CMD_CAT_APP_MGMT = 'Application Management' CMD_CAT_SERVER_INFO = 'Server Information' - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_connect(self, _): - """Connect command""" + def do_connect(self, _) -> None: + """Connect command.""" self.poutput('Connect') # Tag the above command functions under the category Connecting cmd2.categorize(do_connect, CMD_CAT_CONNECTING) @cmd2.with_category(CMD_CAT_CONNECTING) - def do_which(self, _): - """Which command""" + def do_which(self, _) -> None: + """Which command.""" self.poutput('Which') - def do_list(self, _): - """List command""" + def do_list(self, _) -> None: + """List command.""" self.poutput('List') - def do_deploy(self, _): - """Deploy command""" + def do_deploy(self, _) -> None: + """Deploy command.""" self.poutput('Deploy') start_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( @@ -64,16 +62,16 @@ def do_deploy(self, _): @my_decorator @cmd2.with_argparser(start_parser) - def do_start(self, _): - """Start command""" + def do_start(self, _) -> None: + """Start command.""" self.poutput('Start') - def do_sessions(self, _): - """Sessions command""" + def do_sessions(self, _) -> None: + """Sessions command.""" self.poutput('Sessions') - def do_redeploy(self, _): - """Redeploy command""" + def do_redeploy(self, _) -> None: + """Redeploy command.""" self.poutput('Redeploy') restart_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER( @@ -84,24 +82,24 @@ def do_redeploy(self, _): @cmd2.with_argparser(restart_parser) @cmd2.with_category(CMD_CAT_APP_MGMT) @my_decorator - def do_restart(self, _): - """Restart command""" + def do_restart(self, _) -> None: + """Restart command.""" self.poutput('Restart') - def do_expire(self, _): - """Expire command""" + def do_expire(self, _) -> None: + """Expire command.""" self.poutput('Expire') - def do_undeploy(self, _): - """Undeploy command""" + def do_undeploy(self, _) -> None: + """Undeploy command.""" self.poutput('Undeploy') - def do_stop(self, _): - """Stop command""" + def do_stop(self, _) -> None: + """Stop command.""" self.poutput('Stop') - def do_findleakers(self, _): - """Find Leakers command""" + def do_findleakers(self, _) -> None: + """Find Leakers command.""" self.poutput('Find Leakers') # Tag the above command functions under the category Application Management @@ -110,35 +108,34 @@ def do_findleakers(self, _): CMD_CAT_APP_MGMT, ) - def do_resources(self, _): - """Resources command""" + def do_resources(self, _) -> None: + """Resources command.""" self.poutput('Resources') - def do_status(self, _): - """Status command""" + def do_status(self, _) -> None: + """Status command.""" self.poutput('Status') - def do_serverinfo(self, _): - """Server Info command""" + def do_serverinfo(self, _) -> None: + """Server Info command.""" self.poutput('Server Info') - def do_thread_dump(self, _): - """Thread Dump command""" + def do_thread_dump(self, _) -> None: + """Thread Dump command.""" self.poutput('Thread Dump') - def do_sslconnectorciphers(self, _): - """ - SSL Connector Ciphers command is an example of a command that contains + def do_sslconnectorciphers(self, _) -> None: + """SSL Connector Ciphers command is an example of a command that contains multiple lines of help information for the user. Each line of help in a contiguous set of lines will be printed and aligned in the verbose output - provided with 'help --verbose' + provided with 'help --verbose'. This is after a blank line and won't de displayed in the verbose help """ self.poutput('SSL Connector Ciphers') - def do_vminfo(self, _): - """VM Info command""" + def do_vminfo(self, _) -> None: + """VM Info command.""" self.poutput('VM Info') # Tag the above command functions under the category Server Information @@ -151,24 +148,24 @@ def do_vminfo(self, _): # The following command functions don't have the HELP_CATEGORY attribute set # and show up in the 'Other' group - def do_config(self, _): - """Config command""" + def do_config(self, _) -> None: + """Config command.""" self.poutput('Config') - def do_version(self, _): - """Version command""" + def do_version(self, _) -> None: + """Version command.""" self.poutput(cmd2.__version__) @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) + def do_disable_commands(self, _) -> None: + """Disable the Application Management commands.""" + message_to_print = f"{COMMAND_NAME} is not available while {self.CMD_CAT_APP_MGMT} commands are disabled" self.disable_category(self.CMD_CAT_APP_MGMT, message_to_print) self.poutput("The Application Management commands have been disabled") @cmd2.with_category("Command Management") - def do_enable_commands(self, _): - """Enable the Application Management commands""" + def do_enable_commands(self, _) -> None: + """Enable the Application Management commands.""" self.enable_category(self.CMD_CAT_APP_MGMT) self.poutput("The Application Management commands have been enabled") diff --git a/examples/hooks.py b/examples/hooks.py index 97b90739d..ccb9a8386 100755 --- a/examples/hooks.py +++ b/examples/hooks.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2 demonstrating how to use hooks. +"""A sample application for cmd2 demonstrating how to use hooks. This application shows how to use postparsing hooks to allow case insensitive command names, abbreviated commands, as well as allowing numeric arguments to @@ -10,9 +8,6 @@ """ import re -from typing import ( - List, -) import cmd2 @@ -43,8 +38,8 @@ class CmdLineApp(cmd2.Cmd): """ # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist - # default_to_shell = True - def __init__(self, *args, **kwargs): + # default_to_shell = True # noqa: ERA001 + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # register four hooks @@ -85,7 +80,7 @@ def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.Postpa return data def abbrev_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - """Accept unique abbreviated commands""" + """Accept unique abbreviated commands.""" func = self.cmd_func(data.statement.command) if func is None: # check if the entered command might be an abbreviation @@ -96,13 +91,13 @@ def abbrev_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.Postpars return data def proof_hook(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.PostcommandData: - """Update the shell prompt with the new raw statement after postparsing hooks are finished""" + """Update the shell prompt with the new raw statement after postparsing hooks are finished.""" if self.debug: self.prompt = f'({data.statement.raw})' return data @cmd2.with_argument_list - def do_list(self, arglist: List[str]) -> None: + def do_list(self, arglist: list[str]) -> None: """Generate a list of 10 numbers.""" if arglist: first = arglist[0] diff --git a/examples/initialization.py b/examples/initialization.py index 8cdf07341..22de3ff20 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# coding=utf-8 """A simple example cmd2 application demonstrating the following: 1) Colorizing/stylizing output 2) Using multiline commands @@ -10,7 +9,7 @@ 7) Allowing access to your application in py and ipy 8) Displaying an intro banner upon starting your application 9) Using a custom prompt -10) How to make custom attributes settable at runtime +10) How to make custom attributes settable at runtime. """ import cmd2 @@ -24,7 +23,7 @@ class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' - def __init__(self): + def __init__(self) -> None: super().__init__( multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', @@ -57,13 +56,13 @@ def __init__(self): ) @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _): - """Display the intro banner""" + def do_intro(self, _) -> None: + """Display the intro banner.""" self.poutput(self.intro) @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg): - """Example of a multiline command""" + def do_echo(self, arg) -> None: + """Example of a multiline command.""" fg_color = Fg[self.foreground_color.upper()] self.poutput(style(arg, fg=fg_color)) diff --git a/examples/migrating.py b/examples/migrating.py index 199b78db7..55740fa34 100755 --- a/examples/migrating.py +++ b/examples/migrating.py @@ -1,10 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample cmd application that shows how to trivially migrate a cmd application to use cmd2. -""" +"""A sample cmd application that shows how to trivially migrate a cmd application to use cmd2.""" -# import cmd2 as cmd +# import cmd2 as cmd # noqa: ERA001 import cmd # Comment this line and uncomment the one above to migrate to cmd2 import random @@ -16,20 +13,20 @@ class CmdLineApp(cmd.Cmd): MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] - def do_exit(self, line): - """Exit the application""" + def do_exit(self, line) -> bool: + """Exit the application.""" return True - do_EOF = do_exit + do_EOF = do_exit # noqa: N815 do_quit = do_exit - def do_speak(self, line): + def do_speak(self, line) -> None: """Repeats what you tell me to.""" print(line, file=self.stdout) do_say = do_speak - def do_mumble(self, line): + def do_mumble(self, line) -> None: """Mumbles what you tell me to.""" words = line.split(' ') output = [] diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index 8587b98d7..d90e56ae0 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -1,11 +1,4 @@ -# coding=utf-8 -""" -A simple example demonstrating a loadable command set -""" - -from typing import ( - List, -) +"""A simple example demonstrating a loadable command set.""" from cmd2 import ( CommandSet, @@ -35,12 +28,12 @@ def do_flag_based(self, statement: Statement) -> None: """Tab completes arguments based on a preceding flag using flag_based_complete -f, --food [completes food items] -s, --sport [completes sports] - -p, --path [completes local file system paths] + -p, --path [completes local file system paths]. """ - self._cmd.poutput("Args: {}".format(statement.args)) + self._cmd.poutput(f"Args: {statement.args}") - def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """Completion function for do_flag_based""" + def complete_flag_based(self, 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, @@ -56,11 +49,11 @@ def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> return self._cmd.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) def do_index_based(self, statement: Statement) -> None: - """Tab completes first 3 arguments using index_based_complete""" - self._cmd.poutput("Args: {}".format(statement.args)) + """Tab completes first 3 arguments using index_based_complete.""" + self._cmd.poutput(f"Args: {statement.args}") - def complete_index_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """Completion function for do_index_based""" + def complete_index_based(self, 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 @@ -70,19 +63,18 @@ def complete_index_based(self, text: str, line: str, begidx: int, endidx: int) - return self._cmd.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) def do_delimiter_complete(self, statement: Statement) -> None: - """Tab completes files from a list using delimiter_complete""" - self._cmd.poutput("Args: {}".format(statement.args)) + """Tab completes files from a list using delimiter_complete.""" + self._cmd.poutput(f"Args: {statement.args}") - def complete_delimiter_complete(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_delimiter_complete(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self._cmd.delimiter_complete(text, line, begidx, endidx, match_against=self.file_strs, delimiter='/') def do_raise_error(self, statement: Statement) -> None: - """Demonstrates effect of raising CompletionError""" - self._cmd.poutput("Args: {}".format(statement.args)) + """Demonstrates effect of raising CompletionError.""" + self._cmd.poutput(f"Args: {statement.args}") - def complete_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: - """ - CompletionErrors can be raised if an error occurs while tab completing. + def complete_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: + """CompletionErrors can be raised if an error occurs while tab completing. Example use cases - Reading a database to retrieve a tab completion data set failed diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py index 7ab84ac3f..8a5b86c6e 100644 --- a/examples/modular_commands/commandset_complex.py +++ b/examples/modular_commands/commandset_complex.py @@ -1,47 +1,40 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Test CommandSet -""" +"""Test CommandSet.""" import argparse -from typing import ( - List, -) import cmd2 @cmd2.with_default_category('Fruits') class CommandSetA(cmd2.CommandSet): - def do_apple(self, statement: cmd2.Statement): + def do_apple(self, statement: cmd2.Statement) -> None: self._cmd.poutput('Apple!') - def do_banana(self, statement: cmd2.Statement): - """Banana Command""" + def do_banana(self, statement: cmd2.Statement) -> None: + """Banana Command.""" self._cmd.poutput('Banana!!') cranberry_parser = cmd2.Cmd2ArgumentParser() cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) - def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): - self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) + def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]) -> None: + self._cmd.poutput(f'Cranberry {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} - def help_cranberry(self): + def help_cranberry(self) -> None: self._cmd.stdout.write('This command does diddly squat...\n') @cmd2.with_argument_list @cmd2.with_category('Also Alone') - def do_durian(self, args: List[str]): - """Durian Command""" - self._cmd.poutput('{} Arguments: '.format(len(args))) + def do_durian(self, args: list[str]) -> None: + """Durian Command.""" + self._cmd.poutput(f'{len(args)} Arguments: ') self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args)) - def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) elderberry_parser = cmd2.Cmd2ArgumentParser() @@ -49,5 +42,5 @@ def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> Lis @cmd2.with_category('Alone') @cmd2.with_argparser(elderberry_parser) - def do_elderberry(self, ns: argparse.Namespace): - self._cmd.poutput('Elderberry {}!!'.format(ns.arg1)) + def do_elderberry(self, ns: argparse.Namespace) -> None: + self._cmd.poutput(f'Elderberry {ns.arg1}!!') diff --git a/examples/modular_commands/commandset_custominit.py b/examples/modular_commands/commandset_custominit.py index a3f4f59ad..90228016e 100644 --- a/examples/modular_commands/commandset_custominit.py +++ b/examples/modular_commands/commandset_custominit.py @@ -1,7 +1,4 @@ -# coding=utf-8 -""" -A simple example demonstrating a loadable command set -""" +"""A simple example demonstrating a loadable command set.""" from cmd2 import ( Cmd, @@ -13,14 +10,14 @@ @with_default_category('Custom Init') class CustomInitCommandSet(CommandSet): - def __init__(self, arg1, arg2): + def __init__(self, arg1, arg2) -> None: super().__init__() self._arg1 = arg1 self._arg2 = arg2 - def do_show_arg1(self, cmd: Cmd, _: Statement): + def do_show_arg1(self, cmd: Cmd, _: Statement) -> None: self._cmd.poutput('Arg1: ' + self._arg1) - def do_show_arg2(self, cmd: Cmd, _: Statement): + def do_show_arg2(self, cmd: Cmd, _: Statement) -> None: self._cmd.poutput('Arg2: ' + self._arg2) diff --git a/examples/modular_commands_basic.py b/examples/modular_commands_basic.py index 4d5f83cee..7b8f26dba 100755 --- a/examples/modular_commands_basic.py +++ b/examples/modular_commands_basic.py @@ -1,8 +1,5 @@ #!/usr/bin/env python3 -# coding=utf-8 -""" -Simple example demonstrating basic CommandSet usage. -""" +"""Simple example demonstrating basic CommandSet usage.""" import cmd2 from cmd2 import ( @@ -13,25 +10,23 @@ @with_default_category('My Category') class AutoLoadCommandSet(CommandSet): - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_hello(self, _: cmd2.Statement): + def do_hello(self, _: cmd2.Statement) -> None: self._cmd.poutput('Hello') - def do_world(self, _: cmd2.Statement): + def do_world(self, _: cmd2.Statement) -> None: self._cmd.poutput('World') class ExampleApp(cmd2.Cmd): - """ - CommandSets are automatically loaded. Nothing needs to be done. - """ + """CommandSets are automatically loaded. Nothing needs to be done.""" - def __init__(self): - super(ExampleApp, self).__init__() + def __init__(self) -> None: + super().__init__() - def do_something(self, arg): + def do_something(self, arg) -> None: self.poutput('this is the something command') diff --git a/examples/modular_commands_dynamic.py b/examples/modular_commands_dynamic.py index 8264c068f..163c9dc8a 100755 --- a/examples/modular_commands_dynamic.py +++ b/examples/modular_commands_dynamic.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -# coding=utf-8 -""" -Simple example demonstrating dynamic CommandSet loading and unloading. +"""Simple example demonstrating dynamic CommandSet loading and unloading. There are 2 CommandSets defined. ExampleApp sets the `auto_load_commands` flag to false. @@ -22,34 +20,32 @@ @with_default_category('Fruits') class LoadableFruits(CommandSet): - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_apple(self, _: cmd2.Statement): + def do_apple(self, _: cmd2.Statement) -> None: self._cmd.poutput('Apple') - def do_banana(self, _: cmd2.Statement): + def do_banana(self, _: cmd2.Statement) -> None: self._cmd.poutput('Banana') @with_default_category('Vegetables') class LoadableVegetables(CommandSet): - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_arugula(self, _: cmd2.Statement): + def do_arugula(self, _: cmd2.Statement) -> None: self._cmd.poutput('Arugula') - def do_bokchoy(self, _: cmd2.Statement): + def do_bokchoy(self, _: cmd2.Statement) -> None: self._cmd.poutput('Bok Choy') class ExampleApp(cmd2.Cmd): - """ - CommandSets are loaded via the `load` and `unload` commands - """ + """CommandSets are loaded via the `load` and `unload` commands.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, auto_load_commands=False, **kwargs) @@ -61,7 +57,7 @@ def __init__(self, *args, **kwargs): @with_argparser(load_parser) @with_category('Command Loading') - def do_load(self, ns: argparse.Namespace): + def do_load(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': try: self.register_command_set(self._fruits) @@ -77,7 +73,7 @@ def do_load(self, ns: argparse.Namespace): self.poutput('Vegetables already loaded') @with_argparser(load_parser) - def do_unload(self, ns: argparse.Namespace): + def do_unload(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': self.unregister_command_set(self._fruits) self.poutput('Fruits unloaded') diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index e544b3db0..f03ea38d6 100755 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -1,16 +1,11 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A complex example demonstrating a variety of methods to load CommandSets using a mix of command decorators +"""A complex example demonstrating a variety of methods to load CommandSets using a mix of command decorators with examples of how to integrate tab completion with argparse-based commands. """ import argparse -from typing import ( - Iterable, - List, - Optional, -) +from collections.abc import Iterable +from typing import Optional from modular_commands.commandset_basic import ( # noqa: F401 BasicCompletionCommandSet, @@ -18,7 +13,7 @@ from modular_commands.commandset_complex import ( # noqa: F401 CommandSetA, ) -from modular_commands.commandset_custominit import ( # noqa: F401 +from modular_commands.commandset_custominit import ( CustomInitCommandSet, ) @@ -31,12 +26,12 @@ class WithCommandSets(Cmd): - def __init__(self, command_sets: Optional[Iterable[CommandSet]] = None): + def __init__(self, command_sets: Optional[Iterable[CommandSet]] = None) -> None: super().__init__(command_sets=command_sets) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - def choices_provider(self) -> List[str]: - """A choices provider is useful when the choice list is based on instance data of your application""" + def choices_provider(self) -> list[str]: + """A choices provider is useful when the choice list is based on instance data of your application.""" return self.sport_item_strs # Parser for example command @@ -60,7 +55,7 @@ def choices_provider(self) -> List[str]: @with_argparser(example_parser) def do_example(self, _: argparse.Namespace) -> None: - """The example command""" + """The example command.""" self.poutput("I do nothing") diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index 14d117814..f1dbd024c 100755 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# coding=utf-8 -"""A simple example demonstrating modular subcommand loading through CommandSets +"""A simple example demonstrating modular subcommand loading through CommandSets. In this example, there are loadable CommandSets defined. Each CommandSet has 1 subcommand defined that will be attached to the 'cut' command. @@ -24,10 +23,10 @@ @with_default_category('Fruits') class LoadableFruits(CommandSet): - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_apple(self, _: cmd2.Statement): + def do_apple(self, _: cmd2.Statement) -> None: self._cmd.poutput('Apple') banana_description = "Cut a banana" @@ -35,17 +34,17 @@ def do_apple(self, _: cmd2.Statement): banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help=banana_description.lower()) - def cut_banana(self, ns: argparse.Namespace): - """Cut banana""" + def cut_banana(self, ns: argparse.Namespace) -> None: + """Cut banana.""" self._cmd.poutput('cutting banana: ' + ns.direction) @with_default_category('Vegetables') class LoadableVegetables(CommandSet): - def __init__(self): + def __init__(self) -> None: super().__init__() - def do_arugula(self, _: cmd2.Statement): + def do_arugula(self, _: cmd2.Statement) -> None: self._cmd.poutput('Arugula') bokchoy_description = "Cut some bokchoy" @@ -53,16 +52,14 @@ def do_arugula(self, _: cmd2.Statement): bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower()) - def cut_bokchoy(self, _: argparse.Namespace): + def cut_bokchoy(self, _: argparse.Namespace) -> None: self._cmd.poutput('Bok Choy') class ExampleApp(cmd2.Cmd): - """ - CommandSets are automatically loaded. Nothing needs to be done. - """ + """CommandSets are automatically loaded. Nothing needs to be done.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, auto_load_commands=False, **kwargs) @@ -74,7 +71,7 @@ def __init__(self, *args, **kwargs): @with_argparser(load_parser) @with_category('Command Loading') - def do_load(self, ns: argparse.Namespace): + def do_load(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': try: self.register_command_set(self._fruits) @@ -90,7 +87,7 @@ def do_load(self, ns: argparse.Namespace): self.poutput('Vegetables already loaded') @with_argparser(load_parser) - def do_unload(self, ns: argparse.Namespace): + def do_unload(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': self.unregister_command_set(self._fruits) self.poutput('Fruits unloaded') @@ -103,7 +100,7 @@ def do_unload(self, ns: argparse.Namespace): cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') @with_argparser(cut_parser) - def do_cut(self, ns: argparse.Namespace): + def do_cut(self, ns: argparse.Namespace) -> None: # Call handler for whatever subcommand was selected handler = ns.cmd2_handler.get() if handler is not None: diff --git a/examples/override_parser.py b/examples/override_parser.py index 0ae279e70..2d4a0f9ca 100755 --- a/examples/override_parser.py +++ b/examples/override_parser.py @@ -1,8 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -# flake8: noqa F402 -""" -The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser. +"""The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser. The following code shows how to override it with your own parser class. """ @@ -15,9 +12,7 @@ # Next import from cmd2. It will import your module just before the cmd2.Cmd class file is imported # and therefore override the parser class it uses on its commands. -from cmd2 import ( - cmd2, -) +from cmd2 import cmd2 # noqa: E402 if __name__ == '__main__': import sys diff --git a/examples/paged_output.py b/examples/paged_output.py index 0f7173b2e..935bdd2e9 100755 --- a/examples/paged_output.py +++ b/examples/paged_output.py @@ -1,11 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating the using paged output via the ppaged() method.""" import os -from typing import ( - List, -) import cmd2 @@ -13,21 +9,21 @@ class PagedOutput(cmd2.Cmd): """Example cmd2 application which shows how to display output using a pager.""" - def __init__(self): + def __init__(self) -> None: super().__init__() - def page_file(self, file_path: str, chop: bool = False): + def page_file(self, file_path: str, chop: bool = False) -> None: """Helper method to prevent having too much duplicated code.""" filename = os.path.expanduser(file_path) try: - with open(filename, 'r') as f: + with open(filename) as f: text = f.read() self.ppaged(text, chop=chop) except OSError as ex: - self.pexcept('Error reading {!r}: {}'.format(filename, ex)) + self.pexcept(f'Error reading {filename!r}: {ex}') @cmd2.with_argument_list - def do_page_wrap(self, args: List[str]): + def do_page_wrap(self, args: list[str]) -> None: """Read in a text file and display its output in a pager, wrapping long lines if they don't fit. Usage: page_wrap @@ -40,7 +36,7 @@ def do_page_wrap(self, args: List[str]): complete_page_wrap = cmd2.Cmd.path_complete @cmd2.with_argument_list - def do_page_truncate(self, args: List[str]): + def do_page_truncate(self, args: list[str]) -> None: """Read in a text file and display its output in a pager, truncating long lines if they don't fit. Truncated lines can still be accessed by scrolling to the right using the arrow keys. diff --git a/examples/persistent_history.py b/examples/persistent_history.py index ab4b89f2b..d2ae8ceff 100755 --- a/examples/persistent_history.py +++ b/examples/persistent_history.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """This example demonstrates how to enable persistent readline history in your cmd2 application. This will allow end users of your cmd2-based application to use the arrow keys and Ctrl+r in a manner which persists @@ -12,7 +11,7 @@ class Cmd2PersistentHistory(cmd2.Cmd): """Basic example of how to enable persistent readline history within your cmd2 app.""" - def __init__(self, hist_file): + def __init__(self, hist_file) -> None: """Configure the app to load persistent history from a file (both readline and cmd2 history command affected). :param hist_file: file to load history from at start and write it to at end diff --git a/examples/pirate.py b/examples/pirate.py index 8c443d368..75c004da6 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding=utf-8 -""" -This example is adapted from the pirate8.py example created by Catherine Devlin and +"""This example is adapted from the pirate8.py example created by Catherine Devlin and presented as part of her PyCon 2010 talk. It demonstrates many features of cmd2. @@ -21,8 +19,8 @@ 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""" + def __init__(self) -> None: + """Initialize the base class as well as this one.""" shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({'~': 'sing'}) super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts) @@ -46,34 +44,35 @@ def precmd(self, line): 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(f'Now we gots {self.gold} doubloons') if self.gold < 0: self.poutput("Off to debtorrr's prison.") self.exit_code = 1 stop = True return stop - def do_loot(self, arg): + def do_loot(self, arg) -> None: """Seize booty from a passing ship.""" self.gold += 1 - def do_drink(self, arg): + def do_drink(self, arg) -> None: """Drown your sorrrows in rrrum. - drink [n] - drink [n] barrel[s] o' rum.""" + drink [n] - drink [n] barrel[s] o' rum. + """ try: self.gold -= int(arg) except ValueError: if arg: - self.poutput('''What's "{0}"? I'll take rrrum.'''.format(arg)) + self.poutput(f'''What's "{arg}"? I'll take rrrum.''') self.gold -= 1 - def do_quit(self, arg): + def do_quit(self, arg) -> bool: """Quit the application gracefully.""" self.poutput("Quiterrr!") return True - def do_sing(self, arg): + def do_sing(self, arg) -> None: """Sing a colorful song.""" self.poutput(cmd2.ansi.style(arg, fg=Fg[self.songcolor.upper()])) @@ -83,12 +82,12 @@ def do_sing(self, arg): yo_parser.add_argument('beverage', help='beverage to drink with the chant') @cmd2.with_argparser(yo_parser) - def do_yo(self, args): + def do_yo(self, args) -> None: """Compose a yo-ho-ho type chant with flexible options.""" 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(f'{chant} and a bottle of {args.beverage}') if __name__ == '__main__': @@ -97,5 +96,5 @@ def do_yo(self, args): # Create an instance of the Pirate derived class and enter the REPL with cmdloop(). pirate = Pirate() sys_exit_code = pirate.cmdloop() - print('Exiting with code: {!r}'.format(sys_exit_code)) + print(f'Exiting with code: {sys_exit_code!r}') sys.exit(sys_exit_code) diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 688ef52ce..037e39e6c 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """A sample application for how Python scripting can provide conditional control flow of a cmd2 application. @@ -30,13 +29,13 @@ class CmdLineApp(cmd2.Cmd): """Example cmd2 application to showcase conditional control flow in Python scripting within cmd2 apps.""" - def __init__(self): + def __init__(self) -> None: # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell super().__init__(include_ipy=True) self._set_prompt() self.intro = 'Happy 𝛑 Day. Note the full Unicode support: 😇 💩' - def _set_prompt(self): + def _set_prompt(self) -> None: """Set prompt so it displays the current working directory.""" self.cwd = os.getcwd() self.prompt = ansi.style(f'{self.cwd} $ ', fg=ansi.Fg.CYAN) @@ -53,10 +52,10 @@ def postcmd(self, stop: bool, line: str) -> bool: return stop @cmd2.with_argument_list - def do_cd(self, arglist): + def do_cd(self, arglist) -> None: """Change directory. Usage: - cd + cd . """ # Expect 1 argument, the directory to change to if not arglist or len(arglist) != 1: @@ -78,7 +77,7 @@ def do_cd(self, arglist): else: try: os.chdir(path) - except Exception as ex: + except Exception as ex: # noqa: BLE001 err = f'{ex}' else: self.poutput(f'Successfully changed directory to {path}') @@ -97,7 +96,7 @@ def complete_cd(self, text, line, begidx, endidx): dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") @cmd2.with_argparser(dir_parser, with_unknown_args=True) - def do_dir(self, args, unknown): + def do_dir(self, args, unknown) -> None: """List contents of current directory.""" # No arguments for this command if unknown: diff --git a/examples/read_input.py b/examples/read_input.py index bfc43380b..408617705 100755 --- a/examples/read_input.py +++ b/examples/read_input.py @@ -1,12 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion -""" +"""A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion.""" -from typing import ( - List, -) +import contextlib import cmd2 @@ -21,16 +16,14 @@ def __init__(self, *args, **kwargs) -> None: @cmd2.with_category(EXAMPLE_COMMANDS) def do_basic(self, _) -> None: - """Call read_input with no history or tab completion""" + """Call read_input with no history or tab completion.""" self.poutput("Tab completion and up-arrow history is off") - try: + with contextlib.suppress(EOFError): self.read_input("> ") - except EOFError: - pass @cmd2.with_category(EXAMPLE_COMMANDS) def do_basic_with_history(self, _) -> None: - """Call read_input with custom history and no tab completion""" + """Call read_input with custom history and no tab completion.""" self.poutput("Tab completion is off but using custom history") try: input_str = self.read_input("> ", history=self.custom_history) @@ -41,16 +34,14 @@ def do_basic_with_history(self, _) -> None: @cmd2.with_category(EXAMPLE_COMMANDS) def do_commands(self, _) -> None: - """Call read_input the same way cmd2 prompt does to read commands""" + """Call read_input the same way cmd2 prompt does to read commands.""" self.poutput("Tab completing and up-arrow history configured for commands") - try: + with contextlib.suppress(EOFError): self.read_input("> ", completion_mode=cmd2.CompletionMode.COMMANDS) - except EOFError: - pass @cmd2.with_category(EXAMPLE_COMMANDS) def do_custom_choices(self, _) -> None: - """Call read_input to use custom history and choices""" + """Call read_input to use custom history and choices.""" self.poutput("Tab completing with static choices list and using custom history") try: input_str = self.read_input( @@ -64,13 +55,13 @@ def do_custom_choices(self, _) -> None: else: self.custom_history.append(input_str) - def choices_provider(self) -> List[str]: - """Example choices provider function""" + def choices_provider(self) -> list[str]: + """Example choices provider function.""" return ["from_provider_1", "from_provider_2", "from_provider_3"] @cmd2.with_category(EXAMPLE_COMMANDS) def do_custom_choices_provider(self, _) -> None: - """Call read_input to use custom history and choices provider function""" + """Call read_input to use custom history and choices provider function.""" self.poutput("Tab completing with choices from provider function and using custom history") try: input_str = self.read_input( @@ -86,7 +77,7 @@ def do_custom_choices_provider(self, _) -> None: @cmd2.with_category(EXAMPLE_COMMANDS) def do_custom_completer(self, _) -> None: - """Call read_input to use custom history and completer function""" + """Call read_input to use custom history and completer function.""" self.poutput("Tab completing paths and using custom history") try: input_str = self.read_input( @@ -98,7 +89,7 @@ def do_custom_completer(self, _) -> None: @cmd2.with_category(EXAMPLE_COMMANDS) def do_custom_parser(self, _) -> None: - """Call read_input to use a custom history and an argument parser""" + """Call read_input to use a custom history and an argument parser.""" parser = cmd2.Cmd2ArgumentParser(prog='', description="An example parser") parser.add_argument('-o', '--option', help="an optional arg") parser.add_argument('arg_1', help="a choice for this arg", metavar='arg_1', choices=['my_choice', 'your_choice']) diff --git a/examples/remove_builtin_commands.py b/examples/remove_builtin_commands.py index 67541a848..64acd17d8 100755 --- a/examples/remove_builtin_commands.py +++ b/examples/remove_builtin_commands.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating how to remove unused commands. Commands can be removed from help menu and tab completion by appending their command name to the hidden_commands list. @@ -15,7 +14,7 @@ class RemoveBuiltinCommands(cmd2.Cmd): """Example cmd2 application where we remove some unused built-in commands.""" - def __init__(self): + def __init__(self) -> None: super().__init__() # To hide commands from displaying in the help menu, add them to the hidden_commands list diff --git a/examples/remove_settable.py b/examples/remove_settable.py index fad671cbe..c2c338890 100755 --- a/examples/remove_settable.py +++ b/examples/remove_settable.py @@ -1,14 +1,11 @@ #!/usr/bin/env python -# coding=utf-8 -""" -A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters. -""" +"""A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters.""" import cmd2 class MyApp(cmd2.Cmd): - def __init__(self): + def __init__(self) -> None: super().__init__() self.remove_settable('debug') diff --git a/examples/scripts/arg_printer.py b/examples/scripts/arg_printer.py index 924e269ac..aca0f0031 100755 --- a/examples/scripts/arg_printer.py +++ b/examples/scripts/arg_printer.py @@ -1,8 +1,7 @@ #!/usr/bin/env python -# coding=utf-8 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(f"Running Python script {os.path.basename(sys.argv[0])!r} which was called with {len(sys.argv) - 1} arguments") for i, arg in enumerate(sys.argv[1:]): - print("arg {}: {!r}".format(i + 1, arg)) + print(f"arg {i + 1}: {arg!r}") diff --git a/examples/scripts/conditional.py b/examples/scripts/conditional.py index dd3adcca0..99c442de7 100644 --- a/examples/scripts/conditional.py +++ b/examples/scripts/conditional.py @@ -1,7 +1,4 @@ -# coding=utf-8 -# flake8: noqa F821 -""" -This is a Python script intended to be used with the "python_scripting.py" cmd2 example application. +"""This is a Python script intended to be used with the "python_scripting.py" cmd2 example application. To run it you should do the following: ./python_scripting.py @@ -16,36 +13,36 @@ if len(sys.argv) > 1: directory = sys.argv[1] - print('Using specified directory: {!r}'.format(directory)) + print(f'Using specified directory: {directory!r}') else: directory = 'foobar' - print('Using default directory: {!r}'.format(directory)) + print(f'Using default directory: {directory!r}') # Keep track of where we stared original_dir = os.getcwd() # Try to change to the specified directory -result = app('cd {}'.format(directory)) +result = app(f'cd {directory}') # Conditionally do something based on the results of the last command if result: print(f"STDOUT: {result.stdout}\n") print(f"STDERR: {result.stderr}\n") - print('\nContents of directory {!r}:'.format(directory)) + print(f'\nContents of directory {directory!r}:') result = app('dir -l') print(f"STDOUT: {result.stdout}\n") print(f"STDERR: {result.stderr}\n") - print('{}\n'.format(result.data)) + print(f'{result.data}\n') # Change back to where we were - print('Changing back to original directory: {!r}'.format(original_dir)) - app('cd {}'.format(original_dir)) + print(f'Changing back to original directory: {original_dir!r}') + app(f'cd {original_dir}') else: # cd command failed, print a warning - print('Failed to change directory to {!r}'.format(directory)) + print(f'Failed to change directory to {directory!r}') print(f"STDOUT: {result.stdout}\n") print(f"STDERR: {result.stderr}\n") diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py index b8ba9624d..a1e2cdd26 100644 --- a/examples/scripts/save_help_text.py +++ b/examples/scripts/save_help_text.py @@ -1,23 +1,17 @@ -# coding=utf-8 -# flake8: noqa F821 -""" -A cmd2 script that saves the help text for every command, subcommand, and topic to a file. +"""A cmd2 script that saves the help text for every command, subcommand, and topic to a file. This is meant to be run within a cmd2 session using run_pyscript. """ import argparse import os import sys -from typing import ( - List, - TextIO, -) +from typing import TextIO ASTERISKS = "********************************************************" -def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]: - """Get a list of subcommands for an ArgumentParser""" +def get_sub_commands(parser: argparse.ArgumentParser) -> list[str]: + """Get a list of subcommands for an ArgumentParser.""" sub_cmds = [] # Check if this is parser has subcommands @@ -29,9 +23,7 @@ def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]: sub_cmds.append(sub_cmd) # Look for nested subcommands - for nested_sub_cmd in get_sub_commands(sub_cmd_parser): - sub_cmds.append('{} {}'.format(sub_cmd, nested_sub_cmd)) - + sub_cmds.extend(f'{sub_cmd} {nested_sub_cmd}' for nested_sub_cmd in get_sub_commands(sub_cmd_parser)) break sub_cmds.sort() @@ -39,27 +31,22 @@ def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]: def add_help_to_file(item: str, outfile: TextIO, is_command: bool) -> None: - """ - Write help text for commands and topics to the output file + """Write help text for commands and topics to the output file :param item: what is having its help text saved :param outfile: file being written to - :param is_command: tells if the item is a command and not just a help topic + :param is_command: tells if the item is a command and not just a help topic. """ - if is_command: - label = "COMMAND" - else: - label = "TOPIC" + label = "COMMAND" if is_command else "TOPIC" - header = '{}\n{}: {}\n{}\n'.format(ASTERISKS, label, item, ASTERISKS) + header = f'{ASTERISKS}\n{label}: {item}\n{ASTERISKS}\n' outfile.write(header) - result = app('help {}'.format(item)) + result = app(f'help {item}') outfile.write(result.stdout) def main() -> None: - """Main function of this script""" - + """Main function of this script.""" # Make sure we have access to self if 'self' not in globals(): print("Re-run this script from a cmd2 application where self_in_py is True") @@ -67,7 +54,7 @@ def main() -> None: # Make sure the user passed in an output file if len(sys.argv) != 2: - print("Usage: {} ".format(os.path.basename(sys.argv[0]))) + print(f"Usage: {os.path.basename(sys.argv[0])} ") return # Open the output file @@ -75,11 +62,11 @@ def main() -> None: try: outfile = open(outfile_path, 'w') except OSError as e: - print("Error opening {} because: {}".format(outfile_path, e)) + print(f"Error opening {outfile_path} because: {e}") return # Write the help summary - header = '{0}\nSUMMARY\n{0}\n'.format(ASTERISKS) + header = f'{ASTERISKS}\nSUMMARY\n{ASTERISKS}\n' outfile.write(header) result = app('help -v') @@ -98,11 +85,11 @@ def main() -> None: if is_command: # Add any subcommands for subcmd in get_sub_commands(getattr(self.cmd_func(item), 'argparser', None)): - full_cmd = '{} {}'.format(item, subcmd) + full_cmd = f'{item} {subcmd}' add_help_to_file(full_cmd, outfile, is_command) outfile.close() - print("Output written to {}".format(outfile_path)) + print(f"Output written to {outfile_path}") # Run main function diff --git a/examples/scripts/script.py b/examples/scripts/script.py index 339fbf2c8..cca0130c1 100644 --- a/examples/scripts/script.py +++ b/examples/scripts/script.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python -# coding=utf-8 -""" -Trivial example of a Python script which can be run inside a cmd2 application. -""" +"""Trivial example of a Python script which can be run inside a cmd2 application.""" print("This is a python script running ...") diff --git a/examples/subcommands.py b/examples/subcommands.py index 455768e38..b2768cffe 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -# coding=utf-8 """A simple example demonstrating how to use Argparse to support subcommands. - This example shows an easy way for a single command to have many subcommands, each of which takes different arguments and provides separate contextual help. """ @@ -63,26 +61,25 @@ class SubcommandsExample(cmd2.Cmd): - """ - Example cmd2 application where we a base command which has a couple subcommands + """Example cmd2 application where we a base command which has a couple subcommands and the "sport" subcommand has tab completion enabled. """ - def __init__(self): + def __init__(self) -> None: super().__init__() # subcommand functions for the base command - def base_foo(self, args): - """foo subcommand of base command""" + def base_foo(self, args) -> None: + """Foo subcommand of base command.""" self.poutput(args.x * args.y) - def base_bar(self, args): - """bar subcommand of base command""" - self.poutput('((%s))' % args.z) + def base_bar(self, args) -> None: + """Bar subcommand of base command.""" + self.poutput(f'(({args.z}))') - def base_sport(self, args): - """sport subcommand of base command""" - self.poutput('Sport is {}'.format(args.sport)) + def base_sport(self, args) -> None: + """Sport subcommand of base command.""" + self.poutput(f'Sport is {args.sport}') # Set handler functions for the subcommands parser_foo.set_defaults(func=base_foo) @@ -90,8 +87,8 @@ def base_sport(self, args): parser_sport.set_defaults(func=base_sport) @cmd2.with_argparser(base_parser) - def do_base(self, args): - """Base command help""" + def do_base(self, args) -> None: + """Base command help.""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected @@ -101,8 +98,8 @@ def do_base(self, args): self.do_help('base') @cmd2.with_argparser(base2_parser) - def do_alternate(self, args): - """Alternate command help""" + def do_alternate(self, args) -> None: + """Alternate command help.""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected diff --git a/examples/table_creation.py b/examples/table_creation.py index 0849a0b2c..00a45d292 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -1,13 +1,9 @@ #!/usr/bin/env python -# coding=utf-8 -"""Examples of using the cmd2 table creation API""" +"""Examples of using the cmd2 table creation API.""" import functools import sys -from typing import ( - Any, - List, -) +from typing import Any from cmd2 import ( EightBitBg, @@ -30,18 +26,18 @@ class DollarFormatter: - """Example class to show that any object type can be passed as data to TableCreator and converted to a string""" + """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 def __str__(self) -> str: - """Returns the value in dollar currency form (e.g. $100.22)""" - return "${:,.2f}".format(self.val) + """Returns the value in dollar currency form (e.g. $100.22).""" + return f"${self.val:,.2f}" class Relative: - """Class used for example data""" + """Class used for example data.""" def __init__(self, name: str, relationship: str) -> None: self.name = name @@ -49,7 +45,7 @@ def __init__(self, name: str, relationship: str) -> None: class Book: - """Class used for example data""" + """Class used for example data.""" def __init__(self, title: str, year_published: str) -> None: self.title = title @@ -57,26 +53,25 @@ def __init__(self, title: str, year_published: str) -> None: class Author: - """Class used for example data""" + """Class used for example data.""" def __init__(self, name: str, birthday: str, place_of_birth: str) -> None: self.name = name self.birthday = birthday self.place_of_birth = place_of_birth - self.books: List[Book] = [] - self.relatives: List[Relative] = [] + self.books: list[Book] = [] + self.relatives: list[Relative] = [] -def ansi_print(text): - """Wraps style_aware_write so style can be stripped if needed""" +def ansi_print(text) -> None: + """Wraps style_aware_write so style can be stripped if needed.""" ansi.style_aware_write(sys.stdout, text + '\n\n') -def basic_tables(): - """Demonstrates basic examples of the table classes""" - +def basic_tables() -> None: + """Demonstrates basic examples of the table classes.""" # Table data which demonstrates handling of wrapping and text styles - data_list: List[List[Any]] = list() + data_list: list[list[Any]] = [] data_list.append(["Billy Smith", "123 Sesame St.\nFake Town, USA 33445", DollarFormatter(100333.03)]) data_list.append( [ @@ -96,7 +91,7 @@ def basic_tables(): data_list.append(["John Jones", "9235 Highway 32\n" + green("Greenville") + ", SC 29604", DollarFormatter(82987.71)]) # Table Columns (width does not account for any borders or padding which may be added) - columns: List[Column] = list() + columns: list[Column] = [] columns.append(Column("Name", width=20)) columns.append(Column("Address", width=38)) columns.append( @@ -116,14 +111,12 @@ def basic_tables(): ansi_print(table) -def nested_tables(): - """ - Demonstrates how to nest tables with styles which conflict with the parent table by setting style_data_text to False. +def nested_tables() -> None: + """Demonstrates how to nest tables with styles which conflict with the parent table by setting style_data_text to False. It also demonstrates coloring various aspects of tables. """ - # Create data for this example - author_data: List[Author] = [] + author_data: list[Author] = [] author_1 = Author("Frank Herbert", "10/08/1920", "Tacoma, Washington") author_1.books.append(Book("Dune", "1965")) author_1.books.append(Book("Dune Messiah", "1969")) @@ -159,7 +152,7 @@ def nested_tables(): # Define table which presents Author data fields vertically with no header. # This will be nested in the parent table's first column. - author_columns: List[Column] = list() + author_columns: list[Column] = [] author_columns.append(Column("", width=14)) author_columns.append(Column("", width=20)) @@ -174,7 +167,7 @@ def nested_tables(): # Define AlternatingTable for books checked out by people in the first table. # This will be nested in the parent table's second column. - books_columns: List[Column] = list() + books_columns: list[Column] = [] books_columns.append(Column(ansi.style("Title", bold=True), width=25)) books_columns.append( Column( @@ -196,7 +189,7 @@ def nested_tables(): # Define BorderedTable for relatives of the author # This will be nested in the parent table's third column. - relative_columns: List[Column] = list() + relative_columns: list[Column] = [] relative_columns.append(Column(ansi.style("Name", bold=True), width=25)) relative_columns.append(Column(ansi.style("Relationship", bold=True), width=12)) @@ -220,7 +213,7 @@ def nested_tables(): ) # Define parent AlternatingTable which contains Author and Book tables - parent_tbl_columns: List[Column] = list() + parent_tbl_columns: list[Column] = [] # All of the nested tables already have background colors. Set style_data_text # to False so the parent AlternatingTable does not apply background color to them. @@ -242,7 +235,7 @@ def nested_tables(): ) # Construct the tables - parent_table_data: List[List[Any]] = [] + parent_table_data: list[list[Any]] = [] for row, author in enumerate(author_data, start=1): # First build the author table and color it based on row number author_tbl = even_author_tbl if row % 2 == 0 else odd_author_tbl diff --git a/examples/unicode_commands.py b/examples/unicode_commands.py index 6c76a76e7..3321e636f 100755 --- a/examples/unicode_commands.py +++ b/examples/unicode_commands.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding=utf-8 """A simple example demonstrating support for unicode command names.""" import math @@ -10,15 +9,15 @@ class UnicodeApp(cmd2.Cmd): """Example cmd2 application with unicode command names.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.intro = 'Welcome the Unicode example app. Note the full Unicode support: 😇 💩' - def do_𝛑print(self, _): + def do_𝛑print(self, _) -> None: # noqa: PLC2401 """This command prints 𝛑 to 5 decimal places.""" - self.poutput("𝛑 = {0:.6}".format(math.pi)) + self.poutput(f"𝛑 = {math.pi:.6}") - def do_你好(self, arg): + def do_你好(self, arg) -> None: # noqa: N802, PLC2401 """This command says hello in Chinese (Mandarin).""" self.poutput("你好 " + arg) diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py index b154f0ed7..94796e7b3 100644 --- a/plugins/ext_test/cmd2_ext_test/__init__.py +++ b/plugins/ext_test/cmd2_ext_test/__init__.py @@ -1,5 +1,3 @@ -# -# coding=utf-8 """cmd2 External Python Testing Mixin Allows developers to exercise their cmd2 application using the PyScript interface diff --git a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py b/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py index c176bb788..1cb45f603 100644 --- a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py +++ b/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py @@ -1,5 +1,3 @@ -# -# coding=utf-8 """External test interface plugin""" from typing import ( @@ -39,7 +37,8 @@ def app_cmd(self, command: str, echo: Optional[bool] = None) -> cmd2.CommandResu :param echo: Flag whether the command's output should be echoed to stdout/stderr :return: A CommandResult object that captures stdout, stderr, and the command's result object """ - assert isinstance(self, cmd2.Cmd) and isinstance(self, ExternalTestMixin) + assert isinstance(self, cmd2.Cmd) + assert isinstance(self, ExternalTestMixin) try: self._in_py = True @@ -64,7 +63,6 @@ def fixture_teardown(self): :type self: cmd2.Cmd """ - # assert isinstance(self, cmd2.Cmd) and isinstance(self, ExternalTestMixin) for func in self._postloop_hooks: func() self.postloop() diff --git a/plugins/ext_test/examples/example.py b/plugins/ext_test/examples/example.py index 7dbb6677e..11e370450 100644 --- a/plugins/ext_test/examples/example.py +++ b/plugins/ext_test/examples/example.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 -# import cmd2 import cmd2_ext_test import cmd2 diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index 42c6d8a91..b274959c6 100644 --- a/plugins/ext_test/setup.py +++ b/plugins/ext_test/setup.py @@ -1,21 +1,12 @@ -# -# coding=utf-8 - import os import setuptools -# # get the long description from the README file here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() -# scm_version = { -# 'root': '../..', -# 'git_describe_command': "git describe --dirty --tags --long --match plugin-ext-test*", -# } - PACKAGE_DATA = { 'cmd2_ext_test': ['py.typed'], } diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py index 54b0e3791..31fc3f4f0 100644 --- a/plugins/ext_test/tasks.py +++ b/plugins/ext_test/tasks.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 -# flake8: noqa E302 """Development related tasks to be run with 'invoke'. Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI: @@ -27,7 +24,7 @@ def rmrf(items, verbose=True): for item in items: if verbose: - print("Removing {}".format(item)) + print(f"Removing {item}") shutil.rmtree(item, ignore_errors=True) # rmtree doesn't remove bare files try: @@ -51,15 +48,15 @@ def rmrf(items, verbose=True): @invoke.task def pytest(context, junit=False, pty=True, append_cov=False): """Run tests and code coverage using pytest""" - ROOT_PATH = TASK_ROOT.parent.parent + root_path = TASK_ROOT.parent.parent - with context.cd(str(ROOT_PATH)): + with context.cd(str(root_path)): command_str = 'pytest --cov=cmd2_ext_test --cov-report=term --cov-report=html' if append_cov: 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) @@ -147,7 +144,6 @@ def dist_clean(context): def clean_all(context): """Run all clean tasks""" # pylint: disable=unused-argument - pass namespace_clean.add_task(clean_all, 'all') @@ -205,7 +201,7 @@ def lint(context): @invoke.task -def format(context): +def format(context): # noqa: A001 """Run ruff format --check""" with context.cd(TASK_ROOT_STR): context.run("ruff format --check") diff --git a/plugins/ext_test/tests/test_ext_test.py b/plugins/ext_test/tests/test_ext_test.py index 037157f10..df9216d8d 100644 --- a/plugins/ext_test/tests/test_ext_test.py +++ b/plugins/ext_test/tests/test_ext_test.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 - import cmd2_ext_test import pytest diff --git a/plugins/tasks.py b/plugins/tasks.py index 2717ecf4f..b2e2024ee 100644 --- a/plugins/tasks.py +++ b/plugins/tasks.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 -# flake8: noqa E302 """Development related tasks to be run with 'invoke'. Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI: @@ -40,37 +37,33 @@ @invoke.task(pre=[ext_test_tasks.pytest]) @invoke.task() -def pytest(_): - """Run tests and code coverage using pytest""" - pass +def pytest(_) -> None: + """Run tests and code coverage using pytest.""" namespace.add_task(pytest) @invoke.task(pre=[ext_test_tasks.pytest_clean]) -def pytest_clean(_): - """Remove pytest cache and code coverage files and directories""" - pass +def pytest_clean(_) -> None: + """Remove pytest cache and code coverage files and directories.""" namespace_clean.add_task(pytest_clean, 'pytest') @invoke.task(pre=[ext_test_tasks.mypy]) -def mypy(_): - """Run mypy optional static type checker""" - pass +def mypy(_) -> None: + """Run mypy optional static type checker.""" namespace.add_task(mypy) @invoke.task(pre=[ext_test_tasks.mypy_clean]) -def mypy_clean(_): - """Remove mypy cache directory""" +def mypy_clean(_) -> None: + """Remove mypy cache directory.""" # pylint: disable=unused-argument - pass namespace_clean.add_task(mypy_clean, 'mypy') @@ -86,18 +79,16 @@ def mypy_clean(_): @invoke.task(pre=[ext_test_tasks.build_clean]) -def build_clean(_): - """Remove the build directory""" - pass +def build_clean(_) -> None: + """Remove the build directory.""" namespace_clean.add_task(build_clean, 'build') @invoke.task(pre=[ext_test_tasks.dist_clean]) -def dist_clean(_): - """Remove the dist directory""" - pass +def dist_clean(_) -> None: + """Remove the dist directory.""" namespace_clean.add_task(dist_clean, 'dist') @@ -108,28 +99,25 @@ def dist_clean(_): @invoke.task(pre=list(namespace_clean.tasks.values()), default=True) -def clean_all(_): - """Run all clean tasks""" +def clean_all(_) -> None: + """Run all clean tasks.""" # pylint: disable=unused-argument - pass namespace_clean.add_task(clean_all, 'all') @invoke.task(pre=[clean_all], post=[ext_test_tasks.sdist]) -def sdist(_): - """Create a source distribution""" - pass +def sdist(_) -> None: + """Create a source distribution.""" namespace.add_task(sdist) @invoke.task(pre=[clean_all], post=[ext_test_tasks.wheel]) -def wheel(_): - """Build a wheel distribution""" - pass +def wheel(_) -> None: + """Build a wheel distribution.""" namespace.add_task(wheel) @@ -137,7 +125,7 @@ def wheel(_): # ruff linter @invoke.task(pre=[ext_test_tasks.lint]) -def lint(context): +def lint(context) -> None: with context.cd(TASK_ROOT_STR): context.run("ruff check") @@ -147,8 +135,8 @@ def lint(context): # ruff formatter @invoke.task(pre=[ext_test_tasks.format]) -def format(context): - """Run formatter""" +def format(context) -> None: # noqa: A001 + """Run formatter.""" with context.cd(TASK_ROOT_STR): context.run("ruff format --check") diff --git a/plugins/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py index a35976b75..3d4703d54 100644 --- a/plugins/template/cmd2_myplugin/__init__.py +++ b/plugins/template/cmd2_myplugin/__init__.py @@ -1,6 +1,4 @@ -# -# coding=utf-8 -"""Description of myplugin +"""Description of myplugin. An overview of what myplugin does. """ diff --git a/plugins/template/cmd2_myplugin/myplugin.py b/plugins/template/cmd2_myplugin/myplugin.py index 8397e3706..37639a5c2 100644 --- a/plugins/template/cmd2_myplugin/myplugin.py +++ b/plugins/template/cmd2_myplugin/myplugin.py @@ -1,12 +1,8 @@ -# -# coding=utf-8 -"""An example cmd2 plugin""" +"""An example cmd2 plugin.""" import functools -from typing import ( - TYPE_CHECKING, - Callable, -) +from collections.abc import Callable +from typing import TYPE_CHECKING import cmd2 @@ -17,10 +13,10 @@ def empty_decorator(func: Callable) -> Callable: - """An empty decorator for myplugin""" + """An empty decorator for myplugin.""" @functools.wraps(func) - def _empty_decorator(self, *args, **kwargs): + def _empty_decorator(self, *args, **kwargs) -> None: self.poutput("in the empty decorator") func(self, *args, **kwargs) @@ -29,7 +25,7 @@ def _empty_decorator(self, *args, **kwargs): class MyPluginMixin(_Base): - """A mixin class which adds a 'say' command to a cmd2 subclass + """A mixin class which adds a 'say' command to a cmd2 subclass. The order in which you add the mixin matters. Say you want to use this mixin in a class called MyApp. @@ -40,7 +36,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: # code placed here runs before cmd2 initializes super().__init__(*args, **kwargs) # code placed here runs after cmd2 initializes @@ -49,21 +45,21 @@ def __init__(self, *args, **kwargs): self.register_postloop_hook(self.cmd2_myplugin_postloop_hook) self.register_postparsing_hook(self.cmd2_myplugin_postparsing_hook) - def do_say(self, statement): - """Simple say command""" + def do_say(self, statement) -> None: + """Simple say command.""" self.poutput(statement) # # define hooks as functions, not methods def cmd2_myplugin_preloop_hook(self) -> None: - """Method to be called before the command loop begins""" + """Method to be called before the command loop begins.""" self.poutput("preloop hook") def cmd2_myplugin_postloop_hook(self) -> None: - """Method to be called after the command loop finishes""" + """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: - """Method to be called after parsing user input, but before running the command""" + """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 b071b5f84..8a887e20e 100644 --- a/plugins/template/examples/example.py +++ b/plugins/template/examples/example.py @@ -1,20 +1,17 @@ -# -# coding=utf-8 - import cmd2_myplugin import cmd2 class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): - """An class to show how to use a plugin""" + """An class to show how to use a plugin.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: # gotta have this or neither the plugin or cmd2 will initialize super().__init__(*args, **kwargs) @cmd2_myplugin.empty_decorator - def do_something(self, arg): + def do_something(self, arg) -> None: self.poutput('this is the something command') diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py index d8aa344bf..cac9f9177 100644 --- a/plugins/template/noxfile.py +++ b/plugins/template/noxfile.py @@ -2,6 +2,6 @@ @nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13']) -def tests(session): +def tests(session) -> None: session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/template/setup.py b/plugins/template/setup.py index 126f8dde8..3eed7f283 100644 --- a/plugins/template/setup.py +++ b/plugins/template/setup.py @@ -1,11 +1,7 @@ -# -# coding=utf-8 - import os import setuptools -# # get the long description from the README file here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: @@ -13,7 +9,7 @@ setuptools.setup( name='cmd2-myplugin', - # use_scm_version=True, # use_scm_version doesn't work if setup.py isn't in the repository root + # use_scm_version=True, # use_scm_version doesn't work if setup.py isn't in the repository root # noqa: ERA001 version='2.0.0', description='A template used to build plugins for cmd2', long_description=long_description, diff --git a/plugins/template/tasks.py b/plugins/template/tasks.py index ca1058c88..e3e262189 100644 --- a/plugins/template/tasks.py +++ b/plugins/template/tasks.py @@ -1,7 +1,6 @@ -# -# -*- coding: utf-8 -*- -"""Development related tasks to be run with 'invoke'""" +"""Development related tasks to be run with 'invoke'.""" +import contextlib import os import pathlib import shutil @@ -13,20 +12,18 @@ # shared function -def rmrf(items, verbose=True): - """Silently remove a list of directories or files""" +def rmrf(items, verbose=True) -> None: + """Silently remove a list of directories or files.""" if isinstance(items, str): items = [items] for item in items: if verbose: - print("Removing {}".format(item)) + print(f"Removing {item}") shutil.rmtree(item, ignore_errors=True) # rmtree doesn't remove bare files - try: + with contextlib.suppress(FileNotFoundError): os.remove(item) - except FileNotFoundError: - pass # create namespaces @@ -42,17 +39,17 @@ def rmrf(items, verbose=True): @invoke.task -def pytest(context, junit=False, pty=True, append_cov=False): - """Run tests and code coverage using pytest""" - ROOT_PATH = TASK_ROOT.parent.parent +def pytest(context, junit=False, pty=True, append_cov=False) -> None: + """Run tests and code coverage using pytest.""" + root_path = TASK_ROOT.parent.parent - with context.cd(str(ROOT_PATH)): + with context.cd(str(root_path)): command_str = 'pytest --cov=cmd2_myplugin --cov-report=term --cov-report=html' if append_cov: 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) @@ -60,8 +57,8 @@ def pytest(context, junit=False, pty=True, append_cov=False): @invoke.task -def pytest_clean(context): - """Remove pytest cache and code coverage files and directories""" +def pytest_clean(context) -> None: + """Remove pytest cache and code coverage files and directories.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): dirs = ['.pytest_cache', '.cache', '.coverage'] @@ -72,8 +69,8 @@ def pytest_clean(context): @invoke.task -def pylint(context): - """Check code quality using pylint""" +def pylint(context) -> None: + """Check code quality using pylint.""" context.run('pylint --rcfile=cmd2_myplugin/pylintrc cmd2_myplugin') @@ -81,8 +78,8 @@ def pylint(context): @invoke.task -def pylint_tests(context): - """Check code quality of test suite using pylint""" +def pylint_tests(context) -> None: + """Check code quality of test suite using pylint.""" context.run('pylint --rcfile=tests/pylintrc tests') @@ -99,8 +96,8 @@ def pylint_tests(context): @invoke.task -def build_clean(context): - """Remove the build directory""" +def build_clean(context) -> None: + """Remove the build directory.""" # pylint: disable=unused-argument rmrf(BUILDDIR) @@ -109,8 +106,8 @@ def build_clean(context): @invoke.task -def dist_clean(context): - """Remove the dist directory""" +def dist_clean(context) -> None: + """Remove the dist directory.""" # pylint: disable=unused-argument rmrf(DISTDIR) @@ -119,8 +116,8 @@ def dist_clean(context): @invoke.task -def eggs_clean(context): - """Remove egg directories""" +def eggs_clean(context) -> None: + """Remove egg directories.""" # pylint: disable=unused-argument dirs = set() dirs.add('.eggs') @@ -136,8 +133,8 @@ def eggs_clean(context): @invoke.task -def bytecode_clean(context): - """Remove __pycache__ directories and *.pyc files""" +def bytecode_clean(context) -> None: + """Remove __pycache__ directories and *.pyc files.""" # pylint: disable=unused-argument dirs = set() for root, dirnames, files in os.walk(os.curdir): @@ -158,18 +155,17 @@ def bytecode_clean(context): @invoke.task(pre=list(namespace_clean.tasks.values()), default=True) -def clean_all(context): - """Run all clean tasks""" +def clean_all(context) -> None: + """Run all clean tasks.""" # pylint: disable=unused-argument - pass namespace_clean.add_task(clean_all, 'all') @invoke.task(pre=[clean_all]) -def sdist(context): - """Create a source distribution""" +def sdist(context) -> None: + """Create a source distribution.""" context.run('python -m build --sdist') @@ -177,14 +173,14 @@ def sdist(context): @invoke.task(pre=[clean_all]) -def wheel(context): - """Build a wheel distribution""" +def wheel(context) -> None: + """Build a wheel distribution.""" context.run('python -m build --wheel') namespace.add_task(wheel) -# + # these two tasks are commented out so you don't # accidentally run them and upload this template to pypi # @@ -192,11 +188,11 @@ def wheel(context): # @invoke.task(pre=[sdist, wheel]) # def pypi(context): # """Build and upload a distribution to pypi""" -# context.run('twine upload dist/*') -# namespace.add_task(pypi) +# context.run('twine upload dist/*') # noqa: ERA001 +# namespace.add_task(pypi) # noqa: ERA001 # @invoke.task(pre=[sdist, wheel]) # def pypi_test(context): -# """Build and upload a distribution to https://test.pypi.org""" -# context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') -# namespace.add_task(pypi_test) +# """Build and upload a distribution to https://test.pypi.org""" # noqa: ERA001 +# context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') # noqa: ERA001 +# namespace.add_task(pypi_test) # noqa: ERA001 diff --git a/plugins/template/tests/test_myplugin.py b/plugins/template/tests/test_myplugin.py index 06ca25670..7386a8b17 100644 --- a/plugins/template/tests/test_myplugin.py +++ b/plugins/template/tests/test_myplugin.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 - import cmd2_myplugin from cmd2 import ( @@ -17,11 +14,11 @@ class MyApp(cmd2_myplugin.MyPluginMixin, cmd2.Cmd): """Simple subclass of cmd2.Cmd with our SayMixin plugin included.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @cmd2_myplugin.empty_decorator - def do_empty(self, args): + def do_empty(self, args) -> None: self.poutput("running the empty command") @@ -38,8 +35,7 @@ def do_empty(self, args): def init_app(): - app = MyApp() - return app + return MyApp() ##### @@ -49,7 +45,7 @@ def init_app(): ##### -def test_say(capsys): +def test_say(capsys) -> None: # call our initialization function instead of using a fixture app = init_app() # run our mixed in command @@ -61,7 +57,7 @@ def test_say(capsys): assert not err -def test_decorator(capsys): +def test_decorator(capsys) -> None: # call our initialization function instead of using a fixture app = init_app() # run one command in the app diff --git a/pyproject.toml b/pyproject.toml index 2ae92f259..0ff6ffa3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,16 +80,16 @@ disallow_untyped_defs = true exclude = [ "^.git/", "^.venv/", - "^build/", # .build directory - "^docs/", # docs directory + "^build/", # .build directory + "^docs/", # docs directory "^dist/", - "^examples/", # examples directory - "^plugins/*", # plugins directory - "^noxfile\\.py$", # nox config file - "setup\\.py$", # any files named setup.py + "^examples/", # examples directory + "^plugins/*", # plugins directory + "^noxfile\\.py$", # nox config file + "setup\\.py$", # any files named setup.py "^site/", - "^tasks\\.py$", # tasks.py invoke config file - "^tests/", # tests directory + "^tasks\\.py$", # tasks.py invoke config file + "^tests/", # tests directory "^tests_isolated/", # tests_isolated directory ] files = ['.'] @@ -156,72 +156,90 @@ output-format = "full" # McCabe complexity (`C901`) by default. select = [ # https://docs.astral.sh/ruff/rules - # "A", # flake8-builtins (variables or arguments shadowing built-ins) - "AIR", # Airflow specific warnings - # "ANN", # flake8-annotations (missing type annotations for arguments or return types) + "A", # flake8-builtins (variables or arguments shadowing built-ins) + # "AIR", # Airflow specific warnings + # "ANN", # flake8-annotations (missing type annotations for arguments or return types) # "ARG", # flake8-unused-arguments (functions or methods with arguments that are never used) "ASYNC", # flake8-async (async await bugs) # "B", # flake8-bugbear (various likely bugs and design issues) - # "BLE", # flake8-blind-except (force more specific exception types than just Exception) - # "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) + "BLE", # flake8-blind-except (force more specific exception types than just Exception) + "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) - # "COM", # flake8-commas (forces commas at the end of every type of iterable/container + "COM", # flake8-commas (forces commas at the end of every type of iterable/container # "CPY", # flake8-copyright (warn about missing copyright notice at top of file - currently in preview) - # "D", # pydocstyle (warn about things like missing docstrings) + # "D", # pydocstyle (warn about things like missing docstrings) # "DOC", # pydoclint (docstring warnings - currently in preview) - "DJ", # flake8-django (Django-specific warnings) - # "DTZ", # flake8-datetimez (warn about datetime calls where no timezone is specified) - "E", # pycodestyle errors (warn about major stylistic issues like mixing spaces and tabs) + # "DJ", # flake8-django (Django-specific warnings) + "DTZ", # flake8-datetimez (warn about datetime calls where no timezone is specified) + "E", # pycodestyle errors (warn about major stylistic issues like mixing spaces and tabs) # "EM", # flake8-errmsg (warn about exceptions that use string literals that aren't assigned to a variable first) - # "ERA", # eradicate (warn about commented-out code) - # "EXE", # flake8-executable (warn about files with a shebang present that aren't executable or vice versa) - "F", # Pyflakes (a bunch of common warnings for things like unused imports, imports shadowed by variables, etc) - "FA", # flake8-future-annotations (warn if certain from __future__ imports are used but missing) - "FAST", # FastAPI specific warnings + "ERA", # eradicate (warn about commented-out code) + "EXE", # flake8-executable (warn about files with a shebang present that aren't executable or vice versa) + "F", # Pyflakes (a bunch of common warnings for things like unused imports, imports shadowed by variables, etc) + "FA", # flake8-future-annotations (warn if certain from __future__ imports are used but missing) + # "FAST", # FastAPI specific warnings # "FBT", # flake8-boolean-trap (force all boolean arguments passed to functions to be keyword arguments and not positional) - # "FIX", # flake8-fixme (warn about lines containing FIXME, TODO, XXX, or HACK) - "FLY", # flynt (automatically convert from old school string .format to f-strings) - # "FURB", # refurb (A tool for refurbishing and modernizing Python codebases) - "G", # flake8-logging-format (warn about logging statements using outdated string formatting methods) - "I", # isort (sort all import statements in the order established by isort) - "ICN", # flake8-import-conventions (force idiomatic import conventions for certain modules typically imported as something else) - # "INP", # flake8-no-pep420 (warn about files in the implicit namespace - i.e. force creation of __init__.py files to make packages) - "INT", # flake8-gettext (warnings that only apply when you are internationalizing your strings) - # "ISC", # flake8-implicit-str-concat (warnings related to implicit vs explicit string concatenation) - # "LOG", # flake8-logging (warn about potential logger issues, but very pedantic) - # "N", # pep8-naming (force idiomatic naming for classes, functions/methods, and variables/arguments) - "NPY", # NumPy specific rules - "PD", # pandas-vet (Pandas specific rules) - # "PERF", # Perflint (warn about performance issues) - # "PGH", # pygrep-hooks (force specific rule codes when ignoring type or linter issues on a line) - # "PIE", # flake8-pie (eliminate unnecessary use of pass, range starting at 0, etc.) - # "PLC", # Pylint Conventions - "PLE", # Pylint Errors + "FIX", # flake8-fixme (warn about lines containing FIXME, TODO, XXX, or HACK) + "FLY", # flynt (automatically convert from old school string .format to f-strings) + "FURB", # refurb (A tool for refurbishing and modernizing Python codebases) + "G", # flake8-logging-format (warn about logging statements using outdated string formatting methods) + "I", # isort (sort all import statements in the order established by isort) + "ICN", # flake8-import-conventions (force idiomatic import conventions for certain modules typically imported as something else) + "INP", # flake8-no-pep420 (warn about files in the implicit namespace - i.e. force creation of __init__.py files to make packages) + "INT", # flake8-gettext (warnings that only apply when you are internationalizing your strings) + "ISC", # flake8-implicit-str-concat (warnings related to implicit vs explicit string concatenation) + "LOG", # flake8-logging (warn about potential logger issues, but very pedantic) + "N", # pep8-naming (force idiomatic naming for classes, functions/methods, and variables/arguments) + # "NPY", # NumPy specific rules + # "PD", # pandas-vet (Pandas specific rules) + "PERF", # Perflint (warn about performance issues) + "PGH", # pygrep-hooks (force specific rule codes when ignoring type or linter issues on a line) + "PIE", # flake8-pie (eliminate unnecessary use of pass, range starting at 0, etc.) + "PLC", # Pylint Conventions + "PLE", # Pylint Errors # "PLR", # Pylint Refactoring suggestions - # "PLW", # Pylint Warnings - # "PT", # flake8-pytest-style (warnings about unit test best practices) - # "PTH", # flake8-use-pathlib (force use of pathlib instead of os.path) - # "PYI", # flake8-pyi (warnings related to type hint best practices) - # "Q", # flake8-quotes (force double quotes) - # "RET", # flake8-return (various warnings related to implicit vs explicit return statements) + "PLW", # Pylint Warnings + "PT", # flake8-pytest-style (warnings about unit test best practices) + # "PTH", # flake8-use-pathlib (force use of pathlib instead of os.path) + "PYI", # flake8-pyi (warnings related to type hint best practices) + "Q", # flake8-quotes (force double quotes) + "RET", # flake8-return (various warnings related to implicit vs explicit return statements) "RSE", # flake8-raise (warn about unnecessary parentheses on raised exceptions) # "RUF", # Ruff-specific rules (miscellaneous grab bag of lint checks specific to Ruff) - # "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) + "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) # "SIM", # flake8-simplify (rules to attempt to simplify code) - # "SLF", # flake8-self (warn when protected members are accessed outside of a class or file) - # "SLOT", # flake8-slots (warn about subclasses that should define __slots__) - "T10", # flake8-debugger (check for pdb traces left in Python code) + # "SLF", # flake8-self (warn when protected members are accessed outside of a class or file) + "SLOT", # flake8-slots (warn about subclasses that should define __slots__) + "T10", # flake8-debugger (check for pdb traces left in Python code) # "T20", # flake8-print (warn about use of `print` or `pprint` - force use of loggers) - # "TC", # flake8-type-checking (type checking warnings) - # "TD", # flake8-todos (force all TODOs to include an author and issue link) + "TC", # flake8-type-checking (type checking warnings) + "TD", # flake8-todos (force all TODOs to include an author and issue link) "TID", # flake8-tidy-imports (extra import rules to check) - # "TRY", # tryceratops (warnings related to exceptions and try/except) - # "UP", # pyupgrade (A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language) - "W", # pycodestyle warnings (warn about minor stylistic issues) - # "YTT", # flake8-2020 (checks for misuse of sys.version or sys.version_info) + # "TRY", # tryceratops (warnings related to exceptions and try/except) + "UP", # pyupgrade (A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language) + "W", # pycodestyle warnings (warn about minor stylistic issues) + "YTT", # flake8-2020 (checks for misuse of sys.version or sys.version_info) ] ignore = [ # `uv run ruff rule E501` for a description of that rule + "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) + "COM819", # Conflicts with ruff format + "D206", # Conflicts with ruff format + "D300", # Conflicts with ruff format + "E111", # Conflicts with ruff format + "E114", # Conflicts with ruff format + "E117", # Conflicts with ruff format + "ISC002", # Conflicts with ruff format + "Q000", # Conflicts with ruff format + "Q001", # Conflicts with ruff format + "Q002", # Conflicts with ruff format + "Q003", # Conflicts with ruff format + "TC006", # Add quotes to type expression in typing.cast() (not needed except for forward references) + "UP007", # Use X | Y for type annotations (requires Python 3.10+) + "UP017", # Use datetime.UTC alias (requires Python 3.11+) + "UP036", # Version block is outdated for minimum Python version (requires ruff target_version set to minimum supported) + "UP038", # Use X | Y in {} call instead of (X, Y) - deprecated due to poor performance (requires Python 3.10+) + "W191", # Conflicts with ruff format ] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -238,22 +256,51 @@ per-file-ignores."cmd2/__init__.py" = [ "F401", # Unused import ] -per-file-ignores."docs/conf.py" = [ - "F401", # Unused import +per-file-ignores."cmd2/argparse_custom.py" = [ + "UP031", # Use format specifiers instead of percent format (auto fix is unsafe) ] -per-file-ignores."examples/override_parser.py" = [ - "E402", # Module level import not at top of file +per-file-ignores."examples/*.py" = [ + "ANN", # Ignore all type annotation rules in examples folder + "D", # Ignore all pydocstyle rules in examples folder + "INP001", # Module is part of an implicit namespace + "PLW2901", # loop variable overwritten inside loop + "S", # Ignore all Security rules in examples folder ] per-file-ignores."examples/scripts/*.py" = [ "F821", # Undefined name `app` ] +per-file-ignores."plugins/*.py" = [ + "ANN", # Ignore all type annotation rules in test folders + "D", # Ignore all pydocstyle rules in test folders + "INP001", # Module is part of an implicit namespace + "S", # Ignore all Security rules in test folders + "SLF", # Ignore all warnings about private or protected member access in test folders +] + +per-file-ignores."tests/*.py" = [ + "ANN", # Ignore all type annotation rules in test folders + "D", # Ignore all pydocstyle rules in test folders + "E501", # Line too long + "S", # Ignore all Security rules in test folders + "SLF", # Ignore all warnings about private or protected member access in test folders +] + per-file-ignores."tests/pyscript/*.py" = [ - "F821", # Undefined name `app` + "F821", # Undefined name `app` + "INP001", # Module is part of an implicit namespace ] +per-file-ignores."tests_isolated/*.py" = [ + "ANN", # Ignore all type annotation rules in test folders + "D", # Ignore all pydocstyle rules in test folders + "S", # Ignore all Security rules in test folders + "SLF", # Ignore all warnings about private or protected member access in test folders +] + + [tool.ruff.format] # Like Black, use double quotes for strings. quote-style = "preserve" diff --git a/tasks.py b/tasks.py index 6d95b3503..e0163c032 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,3 @@ -# -# coding=utf-8 -# flake8: noqa E302 """Development related tasks to be run with 'invoke'. Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI: @@ -9,6 +6,7 @@ - setuptools >= 39.1.0 """ +import contextlib import os import pathlib import re @@ -26,20 +24,18 @@ # shared function -def rmrf(items, verbose=True): - """Silently remove a list of directories or files""" +def rmrf(items, verbose=True) -> None: + """Silently remove a list of directories or files.""" if isinstance(items, str): items = [items] for item in items: if verbose: - print("Removing {}".format(item)) + print(f"Removing {item}") shutil.rmtree(item, ignore_errors=True) # rmtree doesn't remove bare files - try: + with contextlib.suppress(FileNotFoundError): os.remove(item) - except FileNotFoundError: - pass # create namespaces @@ -55,8 +51,8 @@ def rmrf(items, verbose=True): @invoke.task() -def pytest(context, junit=False, pty=True, base=False, isolated=False): - """Run tests and code coverage using pytest""" +def pytest(context, junit=False, pty=True, base=False, isolated=False) -> None: + """Run tests and code coverage using pytest.""" with context.cd(TASK_ROOT_STR): command_str = 'pytest ' command_str += ' --cov=cmd2 ' @@ -74,17 +70,17 @@ def pytest(context, junit=False, pty=True, base=False, isolated=False): context.run(tests_cmd, pty=pty) if 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) + for dir_name in dirnames: + if dir_name.startswith('test_'): + context.run(command_str + ' tests_isolated/' + dir_name) namespace.add_task(pytest) @invoke.task(post=[plugin_tasks.pytest_clean]) -def pytest_clean(context): - """Remove pytest cache and code coverage files and directories""" +def pytest_clean(context) -> None: + """Remove pytest cache and code coverage files and directories.""" # pylint: disable=unused-argument with context.cd(str(TASK_ROOT / 'tests')): dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] @@ -96,8 +92,8 @@ def pytest_clean(context): @invoke.task() -def mypy(context): - """Run mypy optional static type checker""" +def mypy(context) -> None: + """Run mypy optional static type checker.""" with context.cd(TASK_ROOT_STR): context.run("mypy .") @@ -106,8 +102,8 @@ def mypy(context): @invoke.task() -def mypy_clean(context): - """Remove mypy cache directory""" +def mypy_clean(context) -> None: + """Remove mypy cache directory.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): dirs = ['.mypy_cache', 'dmypy.json', 'dmypy.sock'] @@ -127,8 +123,8 @@ def mypy_clean(context): @invoke.task() -def docs(context, builder='html'): - """Build documentation using MkDocs""" +def docs(context, builder='html') -> None: + """Build documentation using MkDocs.""" with context.cd(TASK_ROOT_STR): context.run('mkdocs build', pty=True) @@ -137,8 +133,8 @@ def docs(context, builder='html'): @invoke.task -def docs_clean(context): - """Remove rendered documentation""" +def docs_clean(context) -> None: + """Remove rendered documentation.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): rmrf(DOCS_BUILDDIR) @@ -148,8 +144,8 @@ def docs_clean(context): @invoke.task -def livehtml(context): - """Launch webserver on http://localhost:8000 with rendered documentation""" +def livehtml(context) -> None: + """Launch webserver on http://localhost:8000 with rendered documentation.""" with context.cd(TASK_ROOT_STR): context.run('mkdocs serve', pty=True) @@ -167,8 +163,8 @@ def livehtml(context): @invoke.task(post=[plugin_tasks.build_clean]) -def build_clean(context): - """Remove the build directory""" +def build_clean(context) -> None: + """Remove the build directory.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): rmrf(BUILDDIR) @@ -178,8 +174,8 @@ def build_clean(context): @invoke.task(post=[plugin_tasks.dist_clean]) -def dist_clean(context): - """Remove the dist directory""" +def dist_clean(context) -> None: + """Remove the dist directory.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): rmrf(DISTDIR) @@ -189,8 +185,8 @@ def dist_clean(context): @invoke.task() -def eggs_clean(context): - """Remove egg directories""" +def eggs_clean(context) -> None: + """Remove egg directories.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): dirs = set() @@ -207,8 +203,8 @@ def eggs_clean(context): @invoke.task() -def pycache_clean(context): - """Remove __pycache__ directories""" +def pycache_clean(context) -> None: + """Remove __pycache__ directories.""" # pylint: disable=unused-argument with context.cd(TASK_ROOT_STR): dirs = set() @@ -224,8 +220,8 @@ def pycache_clean(context): # ruff fast linter @invoke.task() -def lint(context): - """Run ruff fast linter""" +def lint(context) -> None: + """Run ruff fast linter.""" with context.cd(TASK_ROOT_STR): context.run("ruff check") @@ -235,8 +231,8 @@ def lint(context): # ruff fast formatter @invoke.task() -def format(context): - """Run ruff format --check""" +def format(context) -> None: # noqa: A001 + """Run ruff format --check.""" with context.cd(TASK_ROOT_STR): context.run("ruff format --check") @@ -245,8 +241,8 @@ def format(context): @invoke.task() -def ruff_clean(context): - """Remove .ruff_cache directory""" +def ruff_clean(context) -> None: + """Remove .ruff_cache directory.""" with context.cd(TASK_ROOT_STR): context.run("ruff clean") @@ -260,30 +256,29 @@ def ruff_clean(context): @invoke.task(pre=clean_tasks, default=True) -def clean_all(_): - """Run all clean tasks""" +def clean_all(_) -> None: + """Run all clean tasks.""" # pylint: disable=unused-argument - pass namespace_clean.add_task(clean_all, 'all') @invoke.task -def tag(context, name, message=''): - """Add a Git tag and push it to origin""" +def tag(context, name, message='') -> None: + """Add a Git tag and push it to origin.""" # If a tag was provided on the command-line, then add a Git tag and push it to origin if name: - context.run('git tag -a {} -m {!r}'.format(name, message)) - context.run('git push origin {}'.format(name)) + context.run(f'git tag -a {name} -m {message!r}') + context.run(f'git push origin {name}') namespace.add_task(tag) @invoke.task() -def validatetag(context): - """Check to make sure that a tag exists for the current HEAD and it looks like a valid version number""" +def validatetag(context) -> None: + """Check to make sure that a tag exists for the current HEAD and it looks like a valid version number.""" # Validate that a Git tag exists for the current commit HEAD result = context.run("git describe --exact-match --tags $(git log -n1 --pretty='%h')") git_tag = result.stdout.rstrip() @@ -292,18 +287,18 @@ def validatetag(context): ver_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)') match = ver_regex.fullmatch(git_tag) if match is None: - print('Tag {!r} does not appear to be a valid version number'.format(git_tag)) + print(f'Tag {git_tag!r} does not appear to be a valid version number') sys.exit(-1) else: - print('Tag {!r} appears to be a valid version number'.format(git_tag)) + print(f'Tag {git_tag!r} appears to be a valid version number') namespace.add_task(validatetag) @invoke.task(pre=[clean_all], post=[plugin_tasks.sdist]) -def sdist(context): - """Create a source distribution""" +def sdist(context) -> None: + """Create a source distribution.""" with context.cd(TASK_ROOT_STR): context.run('python -m build --sdist') @@ -312,8 +307,8 @@ def sdist(context): @invoke.task(pre=[clean_all], post=[plugin_tasks.wheel]) -def wheel(context): - """Build a wheel distribution""" +def wheel(context) -> None: + """Build a wheel distribution.""" with context.cd(TASK_ROOT_STR): context.run('python -m build --wheel') @@ -322,8 +317,8 @@ def wheel(context): @invoke.task(pre=[validatetag, sdist, wheel]) -def pypi(context): - """Build and upload a distribution to pypi""" +def pypi(context) -> None: + """Build and upload a distribution to pypi.""" with context.cd(TASK_ROOT_STR): context.run('twine upload dist/*') @@ -332,8 +327,8 @@ def pypi(context): @invoke.task(pre=[validatetag, sdist, wheel]) -def pypi_test(context): - """Build and upload a distribution to https://test.pypi.org""" +def pypi_test(context) -> None: + """Build and upload a distribution to https://test.pypi.org.""" with context.cd(TASK_ROOT_STR): context.run('twine upload --repository testpypi dist/*') diff --git a/tests/__init__.py b/tests/__init__.py index 037f3866e..e69de29bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +0,0 @@ -# -# -*- coding: utf-8 -*- -# diff --git a/tests/conftest.py b/tests/conftest.py index 644ae7cca..026b62efc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,4 @@ -# coding=utf-8 -""" -Cmd2 unit/functional testing -""" +"""Cmd2 unit/functional testing""" import argparse import sys @@ -10,7 +7,6 @@ redirect_stdout, ) from typing import ( - List, Optional, Union, ) @@ -18,9 +14,7 @@ mock, ) -from pytest import ( - fixture, -) +import pytest import cmd2 from cmd2.rl_utils import ( @@ -32,7 +26,7 @@ def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None + 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. @@ -40,10 +34,7 @@ def verify_help_text( :param help_output: output of help, either as a string or list of strings :param verbose_strings: optional list of verbose strings to search for """ - if isinstance(help_output, str): - help_text = help_output - else: - help_text = ''.join(help_output) + help_text = help_output if isinstance(help_output, str) else ''.join(help_output) commands = cmd2_app.get_visible_commands() for command in commands: assert command in help_text @@ -141,9 +132,8 @@ def run_cmd(app, cmd): try: app.stdout = copy_cmd_stdout - with redirect_stdout(copy_cmd_stdout): - with redirect_stderr(copy_stderr): - app.onecmd_plus_hooks(cmd) + with redirect_stdout(copy_cmd_stdout), redirect_stderr(copy_stderr): + app.onecmd_plus_hooks(cmd) finally: app.stdout = copy_cmd_stdout.inner_stream sys.stdout = saved_sysout @@ -153,7 +143,7 @@ def run_cmd(app, cmd): return normalize(out), normalize(err) -@fixture +@pytest.fixture def base_app(): return cmd2.Cmd(include_py=True, include_ipy=True) @@ -163,8 +153,7 @@ def base_app(): def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: - """ - This is a convenience function to test cmd2.complete() since + """This is a convenience function to test cmd2.complete() since in a unit test environment there is no actual console readline is monitoring. Therefore we use mock to provide readline data to complete(). @@ -189,13 +178,12 @@ def get_endidx(): return endidx # Run the readline tab completion function with readline mocks in place - with mock.patch.object(readline, 'get_line_buffer', get_line): - with mock.patch.object(readline, 'get_begidx', get_begidx): - with mock.patch.object(readline, 'get_endidx', get_endidx): - return app.complete(text, 0) + with mock.patch.object(readline, 'get_line_buffer', get_line), mock.patch.object(readline, 'get_begidx', get_begidx): + with mock.patch.object(readline, 'get_endidx', get_endidx): + return app.complete(text, 0) -def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser: +def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: if not subcmd_names: return action cur_subcmd = subcmd_names.pop(0) diff --git a/tests/pyscript/echo.py b/tests/pyscript/echo.py index d95e19dbc..c5999355a 100644 --- a/tests/pyscript/echo.py +++ b/tests/pyscript/echo.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 # Tests echo argument to app() app.cmd_echo = False diff --git a/tests/pyscript/environment.py b/tests/pyscript/environment.py index f1182c829..758c85002 100644 --- a/tests/pyscript/environment.py +++ b/tests/pyscript/environment.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 # Tests that cmd2 populates __name__, __file__, and sets sys.path[0] to our directory import os import sys @@ -6,16 +5,16 @@ app.cmd_echo = True if __name__ != '__main__': - print("Error: __name__ is: {}".format(__name__)) + print(f"Error: __name__ is: {__name__}") quit() if __file__ != sys.argv[0]: - print("Error: __file__ is: {}".format(__file__)) + print(f"Error: __file__ is: {__file__}") quit() our_dir = os.path.dirname(os.path.abspath(__file__)) if our_dir != sys.path[0]: - print("Error: our_dir is: {}".format(our_dir)) + print(f"Error: our_dir is: {our_dir}") quit() print("PASSED") diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py index 2e69d79f6..480c6cd70 100644 --- a/tests/pyscript/help.py +++ b/tests/pyscript/help.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 app.cmd_echo = True app('help') diff --git a/tests/pyscript/py_locals.py b/tests/pyscript/py_locals.py index 16cb69262..aa1e0893d 100644 --- a/tests/pyscript/py_locals.py +++ b/tests/pyscript/py_locals.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 # Tests how much a pyscript can affect cmd2.Cmd.py_locals del [locals()["test_var"]] diff --git a/tests/pyscript/pyscript_dir.py b/tests/pyscript/pyscript_dir.py index 81814d70b..14a70a316 100644 --- a/tests/pyscript/pyscript_dir.py +++ b/tests/pyscript/pyscript_dir.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 out = dir(app) out.sort() print(out) diff --git a/tests/pyscript/raises_exception.py b/tests/pyscript/raises_exception.py index ab4670890..5595472bf 100644 --- a/tests/pyscript/raises_exception.py +++ b/tests/pyscript/raises_exception.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python -# coding=utf-8 -""" -Example demonstrating what happens when a Python script raises an exception -""" +"""Example demonstrating what happens when a Python script raises an exception""" 1 + 'blue' diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py index 206f356cb..f71234b8e 100644 --- a/tests/pyscript/recursive.py +++ b/tests/pyscript/recursive.py @@ -1,9 +1,4 @@ -#!/usr/bin/env python -# coding=utf-8 -# flake8: noqa F821 -""" -Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed -""" +"""Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed""" import os import sys diff --git a/tests/pyscript/self_in_py.py b/tests/pyscript/self_in_py.py index f0f6271a9..ee26293f6 100644 --- a/tests/pyscript/self_in_py.py +++ b/tests/pyscript/self_in_py.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 # Tests self_in_py in pyscripts if 'self' in globals(): print("I see self") diff --git a/tests/pyscript/stdout_capture.py b/tests/pyscript/stdout_capture.py index 4aa78d537..5cc0cf3a4 100644 --- a/tests/pyscript/stdout_capture.py +++ b/tests/pyscript/stdout_capture.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 # This script demonstrates when output of a command finalization hook is captured by a pyscript app() call import sys diff --git a/tests/pyscript/stop.py b/tests/pyscript/stop.py index 1578b057e..31b587bd2 100644 --- a/tests/pyscript/stop.py +++ b/tests/pyscript/stop.py @@ -1,4 +1,3 @@ -# flake8: noqa F821 app.cmd_echo = True app('help') diff --git a/tests/script.py b/tests/script.py index 339fbf2c8..cca0130c1 100644 --- a/tests/script.py +++ b/tests/script.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python -# coding=utf-8 -""" -Trivial example of a Python script which can be run inside a cmd2 application. -""" +"""Trivial example of a Python script which can be run inside a cmd2 application.""" print("This is a python script running ...") diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 65ec68a9d..329f7e8ed 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit testing for cmd2/ansi.py module -""" +"""Unit testing for cmd2/ansi.py module""" import pytest @@ -13,14 +9,14 @@ HELLO_WORLD = 'Hello, world!' -def test_strip_style(): +def test_strip_style() -> None: base_str = HELLO_WORLD ansi_str = ansi.style(base_str, fg=ansi.Fg.GREEN) assert base_str != ansi_str assert base_str == ansi.strip_style(ansi_str) -def test_style_aware_wcswidth(): +def test_style_aware_wcswidth() -> None: base_str = HELLO_WORLD ansi_str = ansi.style(base_str, fg=ansi.Fg.GREEN) assert ansi.style_aware_wcswidth(HELLO_WORLD) == ansi.style_aware_wcswidth(ansi_str) @@ -29,7 +25,7 @@ def test_style_aware_wcswidth(): assert ansi.style_aware_wcswidth('i have a newline\n') == -1 -def test_widest_line(): +def test_widest_line() -> None: text = ansi.style('i have\n3 lines\nThis is the longest one', fg=ansi.Fg.GREEN) assert ansi.widest_line(text) == ansi.style_aware_wcswidth("This is the longest one") @@ -39,27 +35,27 @@ def test_widest_line(): assert ansi.widest_line('i have a tab\t') == -1 -def test_style_none(): +def test_style_none() -> None: base_str = HELLO_WORLD ansi_str = base_str assert ansi.style(base_str) == ansi_str @pytest.mark.parametrize('fg_color', [ansi.Fg.BLUE, ansi.EightBitFg.AQUAMARINE_1A, ansi.RgbFg(0, 2, 4)]) -def test_style_fg(fg_color): +def test_style_fg(fg_color) -> None: base_str = HELLO_WORLD ansi_str = fg_color + base_str + ansi.Fg.RESET assert ansi.style(base_str, fg=fg_color) == ansi_str @pytest.mark.parametrize('bg_color', [ansi.Bg.BLUE, ansi.EightBitBg.AQUAMARINE_1A, ansi.RgbBg(0, 2, 4)]) -def test_style_bg(bg_color): +def test_style_bg(bg_color) -> None: base_str = HELLO_WORLD ansi_str = bg_color + base_str + ansi.Bg.RESET assert ansi.style(base_str, bg=bg_color) == ansi_str -def test_style_invalid_types(): +def test_style_invalid_types() -> None: # Use a BgColor with fg with pytest.raises(TypeError): ansi.style('test', fg=ansi.Bg.BLUE) @@ -69,43 +65,43 @@ def test_style_invalid_types(): ansi.style('test', bg=ansi.Fg.BLUE) -def test_style_bold(): +def test_style_bold() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.INTENSITY_BOLD + base_str + ansi.TextStyle.INTENSITY_NORMAL assert ansi.style(base_str, bold=True) == ansi_str -def test_style_dim(): +def test_style_dim() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.INTENSITY_DIM + base_str + ansi.TextStyle.INTENSITY_NORMAL assert ansi.style(base_str, dim=True) == ansi_str -def test_style_italic(): +def test_style_italic() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.ITALIC_ENABLE + base_str + ansi.TextStyle.ITALIC_DISABLE assert ansi.style(base_str, italic=True) == ansi_str -def test_style_overline(): +def test_style_overline() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.OVERLINE_ENABLE + base_str + ansi.TextStyle.OVERLINE_DISABLE assert ansi.style(base_str, overline=True) == ansi_str -def test_style_strikethrough(): +def test_style_strikethrough() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.STRIKETHROUGH_ENABLE + base_str + ansi.TextStyle.STRIKETHROUGH_DISABLE assert ansi.style(base_str, strikethrough=True) == ansi_str -def test_style_underline(): +def test_style_underline() -> None: base_str = HELLO_WORLD ansi_str = ansi.TextStyle.UNDERLINE_ENABLE + base_str + ansi.TextStyle.UNDERLINE_DISABLE assert ansi.style(base_str, underline=True) == ansi_str -def test_style_multi(): +def test_style_multi() -> None: base_str = HELLO_WORLD fg_color = ansi.Fg.LIGHT_BLUE bg_color = ansi.Bg.LIGHT_GRAY @@ -144,13 +140,13 @@ def test_style_multi(): ) -def test_set_title(): +def test_set_title() -> None: title = HELLO_WORLD assert ansi.set_title(title) == ansi.OSC + '2;' + title + ansi.BEL @pytest.mark.parametrize( - 'cols, prompt, line, cursor, msg, expected', + ('cols', 'prompt', 'line', 'cursor', 'msg', 'expected'), [ ( 127, @@ -171,47 +167,49 @@ def test_set_title(): ), ], ) -def test_async_alert_str(cols, prompt, line, cursor, msg, expected): +def test_async_alert_str(cols, prompt, line, cursor, msg, expected) -> None: alert_str = ansi.async_alert_str(terminal_columns=cols, prompt=prompt, line=line, cursor_offset=cursor, alert_msg=msg) assert alert_str == expected -def test_clear_screen(): +def test_clear_screen() -> None: clear_type = 2 assert ansi.clear_screen(clear_type) == f"{ansi.CSI}{clear_type}J" clear_type = -1 - with pytest.raises(ValueError): + expected_err = "clear_type must in an integer from 0 to 3" + with pytest.raises(ValueError, match=expected_err): ansi.clear_screen(clear_type) clear_type = 4 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=expected_err): ansi.clear_screen(clear_type) -def test_clear_line(): +def test_clear_line() -> None: clear_type = 2 assert ansi.clear_line(clear_type) == f"{ansi.CSI}{clear_type}K" clear_type = -1 - with pytest.raises(ValueError): + expected_err = "clear_type must in an integer from 0 to 2" + with pytest.raises(ValueError, match=expected_err): ansi.clear_line(clear_type) clear_type = 3 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=expected_err): ansi.clear_line(clear_type) -def test_cursor(): +def test_cursor() -> None: count = 1 - assert ansi.Cursor.UP(count) == f"{ansi.CSI}{count}A" - assert ansi.Cursor.DOWN(count) == f"{ansi.CSI}{count}B" - assert ansi.Cursor.FORWARD(count) == f"{ansi.CSI}{count}C" - assert ansi.Cursor.BACK(count) == f"{ansi.CSI}{count}D" + assert ansi.Cursor._up(count) == f"{ansi.CSI}{count}A" + assert ansi.Cursor._down(count) == f"{ansi.CSI}{count}B" + assert ansi.Cursor._forward(count) == f"{ansi.CSI}{count}C" + assert ansi.Cursor._back(count) == f"{ansi.CSI}{count}D" x = 4 y = 5 - assert ansi.Cursor.SET_POS(x, y) == f"{ansi.CSI}{y};{x}H" + assert ansi.Cursor._set_pos(x, y) == f"{ansi.CSI}{y};{x}H" @pytest.mark.parametrize( @@ -226,13 +224,13 @@ def test_cursor(): ansi.TextStyle.OVERLINE_ENABLE, ], ) -def test_sequence_str_building(ansi_sequence): +def test_sequence_str_building(ansi_sequence) -> None: """This tests __add__(), __radd__(), and __str__() methods for AnsiSequences""" assert ansi_sequence + ansi_sequence == str(ansi_sequence) + str(ansi_sequence) @pytest.mark.parametrize( - 'r, g, b, valid', + ('r', 'g', 'b', 'valid'), [ (0, 0, 0, True), (255, 255, 255, True), @@ -244,18 +242,19 @@ def test_sequence_str_building(ansi_sequence): (255, 255, 256, False), ], ) -def test_rgb_bounds(r, g, b, valid): +def test_rgb_bounds(r, g, b, valid) -> None: if valid: ansi.RgbFg(r, g, b) ansi.RgbBg(r, g, b) else: - with pytest.raises(ValueError): + expected_err = "RGB values must be integers in the range of 0 to 255" + with pytest.raises(ValueError, match=expected_err): ansi.RgbFg(r, g, b) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=expected_err): ansi.RgbBg(r, g, b) -def test_std_color_re(): +def test_std_color_re() -> None: """Test regular expressions for matching standard foreground and background colors""" for color in ansi.Fg: assert ansi.STD_FG_RE.match(str(color)) @@ -269,7 +268,7 @@ def test_std_color_re(): assert not ansi.STD_BG_RE.match(f'{ansi.CSI}48m') -def test_eight_bit_color_re(): +def test_eight_bit_color_re() -> None: """Test regular expressions for matching eight-bit foreground and background colors""" for color in ansi.EightBitFg: assert ansi.EIGHT_BIT_FG_RE.match(str(color)) @@ -283,7 +282,7 @@ def test_eight_bit_color_re(): assert not ansi.EIGHT_BIT_BG_RE.match(f'{ansi.CSI}48;5;256m') -def test_rgb_color_re(): +def test_rgb_color_re() -> None: """Test regular expressions for matching RGB foreground and background colors""" for i in range(256): fg_color = ansi.RgbFg(i, i, i) diff --git a/tests/test_argparse.py b/tests/test_argparse.py index f800c84a6..df82049f0 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -1,13 +1,7 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Cmd2 testing for argument parsing -""" +"""Cmd2 testing for argument parsing""" import argparse -from typing import ( - Optional, -) +from typing import Optional import pytest @@ -19,7 +13,7 @@ class ArgparseApp(cmd2.Cmd): - def __init__(self): + def __init__(self) -> None: self.maxrepeats = 3 cmd2.Cmd.__init__(self) @@ -38,9 +32,8 @@ def _say_parser_builder() -> cmd2.Cmd2ArgumentParser: return say_parser @cmd2.with_argparser(_say_parser_builder) - def do_say(self, args, *, keyword_arg: Optional[str] = None): - """ - Repeat what you + def do_say(self, args, *, keyword_arg: Optional[str] = None) -> None: + """Repeat what you tell me to. :param args: argparse namespace @@ -48,13 +41,14 @@ def do_say(self, args, *, keyword_arg: Optional[str] = None): """ words = [] for word in args.words: + modified_word = word if word is None: - word = '' + modified_word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + modified_word = f'{word[1:]}{word[0]}ay' if args.shout: - word = word.upper() - words.append(word) + modified_word = word.upper() + words.append(modified_word) repetitions = args.repeat or 1 for i in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) @@ -68,16 +62,16 @@ def do_say(self, args, *, keyword_arg: Optional[str] = None): tag_parser.add_argument('content', nargs='+', help='content to surround with tag') @cmd2.with_argparser(tag_parser, preserve_quotes=True) - def do_tag(self, args): + def do_tag(self, args) -> None: self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) self.stdout.write('\n') @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(), ns_provider=namespace_provider) - def do_test_argparse_ns(self, args): - self.stdout.write('{}'.format(args.custom_stuff)) + def do_test_argparse_ns(self, args) -> None: + self.stdout.write(f'{args.custom_stuff}') @cmd2.with_argument_list - def do_arglist(self, arglist, *, keyword_arg: Optional[str] = None): + def do_arglist(self, arglist, *, keyword_arg: Optional[str] = None) -> None: if isinstance(arglist, list): self.stdout.write('True') else: @@ -87,8 +81,8 @@ def do_arglist(self, arglist, *, keyword_arg: Optional[str] = None): print(keyword_arg) @cmd2.with_argument_list(preserve_quotes=True) - def do_preservelist(self, arglist): - self.stdout.write('{}'.format(arglist)) + def do_preservelist(self, arglist) -> None: + self.stdout.write(f'{arglist}') @classmethod def _speak_parser_builder(cls) -> cmd2.Cmd2ArgumentParser: @@ -99,17 +93,18 @@ def _speak_parser_builder(cls) -> cmd2.Cmd2ArgumentParser: return known_parser @cmd2.with_argparser(_speak_parser_builder, with_unknown_args=True) - def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None): + def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None) -> None: """Repeat what you tell me to.""" words = [] for word in extra: + modified_word = word if word is None: - word = '' + modified_word = '' if args.piglatin: - word = '%s%say' % (word[1:], word[0]) + modified_word = f'{word[1:]}{word[0]}ay' if args.shout: - word = word.upper() - words.append(word) + modified_word = word.upper() + words.append(modified_word) repetitions = args.repeat or 1 for i in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) @@ -119,102 +114,101 @@ def do_speak(self, args, extra, *, keyword_arg: Optional[str] = None): print(keyword_arg) @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(), preserve_quotes=True, with_unknown_args=True) - def do_test_argparse_with_list_quotes(self, args, extra): + def do_test_argparse_with_list_quotes(self, args, extra) -> None: self.stdout.write('{}'.format(' '.join(extra))) @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(), ns_provider=namespace_provider, with_unknown_args=True) - def do_test_argparse_with_list_ns(self, args, extra): - self.stdout.write('{}'.format(args.custom_stuff)) + def do_test_argparse_with_list_ns(self, args, extra) -> None: + self.stdout.write(f'{args.custom_stuff}') @pytest.fixture def argparse_app(): - app = ArgparseApp() - return app + return ArgparseApp() -def test_invalid_syntax(argparse_app): +def test_invalid_syntax(argparse_app) -> None: out, err = run_cmd(argparse_app, 'speak "') assert err[0] == "Invalid syntax: No closing quotation" -def test_argparse_basic_command(argparse_app): +def test_argparse_basic_command(argparse_app) -> None: out, err = run_cmd(argparse_app, 'say hello') assert out == ['hello'] -def test_argparse_remove_quotes(argparse_app): +def test_argparse_remove_quotes(argparse_app) -> None: out, err = run_cmd(argparse_app, 'say "hello there"') assert out == ['hello there'] -def test_argparse_with_no_args(argparse_app): +def test_argparse_with_no_args(argparse_app) -> None: """Make sure we receive TypeError when calling argparse-based function with no args""" with pytest.raises(TypeError) as excinfo: argparse_app.do_say() assert 'Expected arguments' in str(excinfo.value) -def test_argparser_kwargs(argparse_app, capsys): +def test_argparser_kwargs(argparse_app, capsys) -> None: """Test with_argparser wrapper passes through kwargs to command function""" argparse_app.do_say('word', keyword_arg="foo") out, err = capsys.readouterr() assert out == "foo\n" -def test_argparse_preserve_quotes(argparse_app): +def test_argparse_preserve_quotes(argparse_app) -> None: out, err = run_cmd(argparse_app, 'tag mytag "hello"') assert out[0] == '"hello"' -def test_argparse_custom_namespace(argparse_app): +def test_argparse_custom_namespace(argparse_app) -> None: out, err = run_cmd(argparse_app, 'test_argparse_ns') assert out[0] == 'custom' -def test_argparse_with_list(argparse_app): +def test_argparse_with_list(argparse_app) -> None: out, err = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] -def test_argparse_with_list_remove_quotes(argparse_app): +def test_argparse_with_list_remove_quotes(argparse_app) -> None: out, err = run_cmd(argparse_app, 'speak -s hello "world!"') assert out == ['HELLO WORLD!'] -def test_argparse_with_list_preserve_quotes(argparse_app): +def test_argparse_with_list_preserve_quotes(argparse_app) -> None: out, err = run_cmd(argparse_app, 'test_argparse_with_list_quotes "hello" person') assert out[0] == '"hello" person' -def test_argparse_with_list_custom_namespace(argparse_app): +def test_argparse_with_list_custom_namespace(argparse_app) -> None: out, err = run_cmd(argparse_app, 'test_argparse_with_list_ns') assert out[0] == 'custom' -def test_argparse_with_list_and_empty_doc(argparse_app): +def test_argparse_with_list_and_empty_doc(argparse_app) -> None: out, err = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] -def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app): +def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app) -> None: out, err = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!") assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!'] -def test_argparser_and_unknown_args_kwargs(argparse_app, capsys): +def test_argparser_and_unknown_args_kwargs(argparse_app, capsys) -> None: """Test with_argparser wrapper passing through kwargs to command function""" argparse_app.do_speak('', keyword_arg="foo") out, err = capsys.readouterr() assert out == "foo\n" -def test_argparse_quoted_arguments_multiple(argparse_app): +def test_argparse_quoted_arguments_multiple(argparse_app) -> None: out, err = run_cmd(argparse_app, 'say "hello there" "rick & morty"') assert out == ['hello there rick & morty'] -def test_argparse_help_docstring(argparse_app): +def test_argparse_help_docstring(argparse_app) -> None: out, err = run_cmd(argparse_app, 'help say') assert out[0].startswith('Usage: say') assert out[1] == '' @@ -224,32 +218,32 @@ def test_argparse_help_docstring(argparse_app): assert not line.startswith(':') -def test_argparse_help_description(argparse_app): +def test_argparse_help_description(argparse_app) -> None: out, err = run_cmd(argparse_app, 'help tag') assert out[0].startswith('Usage: tag') assert out[1] == '' assert out[2] == 'create a html tag' -def test_argparse_prog(argparse_app): +def test_argparse_prog(argparse_app) -> None: out, err = run_cmd(argparse_app, 'help tag') progname = out[0].split(' ')[1] assert progname == 'tag' -def test_arglist(argparse_app): +def test_arglist(argparse_app) -> None: out, err = run_cmd(argparse_app, 'arglist "we should" get these') assert out[0] == 'True' -def test_arglist_kwargs(argparse_app, capsys): +def test_arglist_kwargs(argparse_app, capsys) -> None: """Test with_argument_list wrapper passes through kwargs to command function""" argparse_app.do_arglist('arg', keyword_arg="foo") out, err = capsys.readouterr() assert out == "foo\n" -def test_preservelist(argparse_app): +def test_preservelist(argparse_app) -> None: out, err = run_cmd(argparse_app, 'preservelist foo "bar baz"') assert out[0] == "['foo', '\"bar baz\"']" @@ -264,17 +258,17 @@ class SubcommandApp(cmd2.Cmd): """Example cmd2 application where we a base command which has a couple subcommands.""" # subcommand functions for the base command - def base_foo(self, args): - """foo subcommand of base command""" + def base_foo(self, args) -> None: + """Foo subcommand of base command""" self.poutput(args.x * args.y) - def base_bar(self, args): - """bar subcommand of base command""" - self.poutput('((%s))' % args.z) + def base_bar(self, args) -> None: + """Bar subcommand of base command""" + self.poutput(f'(({args.z}))') - def base_helpless(self, args): - """helpless subcommand of base command""" - self.poutput('((%s))' % args.z) + def base_helpless(self, args) -> None: + """Helpless subcommand of base command""" + self.poutput(f'(({args.z}))') # create the top-level parser for the base command base_parser = cmd2.Cmd2ArgumentParser() @@ -295,12 +289,12 @@ def base_helpless(self, args): # This subcommand has aliases and no help text. It exists to prevent changes to _set_parser_prog() which # use an approach which relies on action._choices_actions list. See comment in that function for more # details. - parser_bar = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2']) - parser_bar.add_argument('z', help='string') - parser_bar.set_defaults(func=base_bar) + parser_helpless = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2']) + parser_helpless.add_argument('z', help='string') + parser_helpless.set_defaults(func=base_bar) @cmd2.with_argparser(base_parser) - def do_base(self, args): + def do_base(self, args) -> None: """Base command help""" # Call whatever subcommand function was selected func = getattr(args, 'func') @@ -308,14 +302,14 @@ def do_base(self, args): # Add subcommands using as_subcommand_to decorator @cmd2.with_argparser(_build_has_subcmd_parser) - def do_test_subcmd_decorator(self, args: argparse.Namespace): + def do_test_subcmd_decorator(self, args: argparse.Namespace) -> None: handler = args.cmd2_handler.get() handler(args) subcmd_parser = cmd2.Cmd2ArgumentParser(description="A subcommand") @cmd2.as_subcommand_to('test_subcmd_decorator', 'subcmd', subcmd_parser, help=subcmd_parser.description.lower()) - def subcmd_func(self, args: argparse.Namespace): + def subcmd_func(self, args: argparse.Namespace) -> None: # Make sure printing the Namespace works. The way we originally added cmd2_handler to it resulted in a RecursionError. self.poutput(args) @@ -324,41 +318,40 @@ def subcmd_func(self, args: argparse.Namespace): @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): + def helpless_subcmd_func(self, args: argparse.Namespace) -> None: # Make sure vars(Namespace) works. The way we originally added cmd2_handler to it resulted in a RecursionError. self.poutput(vars(args)) @pytest.fixture def subcommand_app(): - app = SubcommandApp() - return app + return SubcommandApp() -def test_subcommand_foo(subcommand_app): +def test_subcommand_foo(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base foo -x2 5.0') assert out == ['10.0'] -def test_subcommand_bar(subcommand_app): +def test_subcommand_bar(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base bar baz') assert out == ['((baz))'] -def test_subcommand_invalid(subcommand_app): +def test_subcommand_invalid(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base baz') assert err[0].startswith('Usage: base') assert err[1].startswith("Error: argument SUBCOMMAND: invalid choice: 'baz'") -def test_subcommand_base_help(subcommand_app): +def test_subcommand_base_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base') assert out[0].startswith('Usage: base') assert out[1] == '' assert out[2] == 'Base command help' -def test_subcommand_help(subcommand_app): +def test_subcommand_help(subcommand_app) -> None: # foo has no aliases out, err = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('Usage: base foo') @@ -398,14 +391,13 @@ def test_subcommand_help(subcommand_app): assert out[2] == 'positional arguments:' -def test_subcommand_invalid_help(subcommand_app): +def test_subcommand_invalid_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('Usage: base') -def test_add_another_subcommand(subcommand_app): - """ - This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls +def test_add_another_subcommand(subcommand_app) -> None: + """This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls to add_parser() write the correct prog value to the parser being added. """ base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base) @@ -417,7 +409,7 @@ def test_add_another_subcommand(subcommand_app): assert new_parser.prog == "base new_sub" -def test_subcmd_decorator(subcommand_app): +def test_subcmd_decorator(subcommand_app) -> None: # Test subcommand that has help option out, err = run_cmd(subcommand_app, 'test_subcmd_decorator subcmd') assert out[0].startswith('Namespace(') @@ -442,7 +434,7 @@ def test_subcmd_decorator(subcommand_app): assert err[1] == 'Error: unrecognized arguments: -h' -def test_unittest_mock(): +def test_unittest_mock() -> None: from unittest import ( mock, ) @@ -451,9 +443,8 @@ def test_unittest_mock(): CommandSetRegistrationError, ) - with mock.patch.object(ArgparseApp, 'namespace_provider'): - with pytest.raises(CommandSetRegistrationError): - ArgparseApp() + with mock.patch.object(ArgparseApp, 'namespace_provider'), pytest.raises(CommandSetRegistrationError): + ArgparseApp() with mock.patch.object(ArgparseApp, 'namespace_provider', spec=True): ArgparseApp() @@ -465,7 +456,7 @@ def test_unittest_mock(): ArgparseApp() -def test_pytest_mock_invalid(mocker): +def test_pytest_mock_invalid(mocker) -> None: from cmd2 import ( CommandSetRegistrationError, ) @@ -483,6 +474,6 @@ def test_pytest_mock_invalid(mocker): {'autospec': True}, ], ) -def test_pytest_mock_valid(mocker, spec_param): +def test_pytest_mock_valid(mocker, spec_param) -> None: mocker.patch.object(ArgparseApp, 'namespace_provider', **spec_param) ArgparseApp() diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 1f9178f88..4605a34b8 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -1,16 +1,8 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit/functional testing for argparse completer in cmd2 -""" +"""Unit/functional testing for argparse completer in cmd2""" import argparse import numbers -from typing import ( - Dict, - List, - cast, -) +from typing import cast import pytest @@ -39,18 +31,18 @@ standalone_completions = ['standalone', 'completer'] -def standalone_choice_provider(cli: cmd2.Cmd) -> List[str]: +def standalone_choice_provider(cli: cmd2.Cmd) -> list[str]: return standalone_choices -def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]: +def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]: return cli.basic_complete(text, line, begidx, endidx, standalone_completions) class ArgparseCompleterTester(cmd2.Cmd): """Cmd2 app that exercises ArgparseCompleter class""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) ############################################################################################################ @@ -115,7 +107,7 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None: TUPLE_METAVAR = ('arg1', 'others') CUSTOM_DESC_HEADER = "Custom Header" - # Lists used in our tests (there is a mix of sorted and unsorted on purpose) + # lists used in our tests (there is a mix of sorted and unsorted on purpose) non_negative_num_choices = [1, 2, 3, 0.5, 22] num_choices = [-1, 1, -2, 2.5, 0, -12] static_choices_list = ['static', 'choices', 'stop', 'here'] @@ -125,15 +117,15 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None: # This tests that CompletionItems created with numerical values are sorted as numbers. num_completion_items = [CompletionItem(5, "Five"), CompletionItem(1.5, "One.Five"), CompletionItem(2, "Five")] - def choices_provider(self) -> List[str]: + def choices_provider(self) -> list[str]: """Method that provides choices""" return self.choices_from_provider - def completion_item_method(self) -> List[CompletionItem]: + def completion_item_method(self) -> list[CompletionItem]: """Choices method that returns CompletionItems""" items = [] - for i in range(0, 10): - main_str = 'main_str{}'.format(i) + for i in range(10): + main_str = f'main_str{i}' items.append(CompletionItem(main_str, description='blah blah')) return items @@ -191,13 +183,13 @@ def do_choices(self, args: argparse.Namespace) -> None: completions_for_pos_1 = ['completions', 'positional_1', 'probably', 'missed', 'spot'] completions_for_pos_2 = ['completions', 'positional_2', 'probably', 'missed', 'me'] - def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_flag) - def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_1) - def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_2) completer_parser = Cmd2ArgumentParser() @@ -265,11 +257,11 @@ def do_hint(self, args: argparse.Namespace) -> None: ############################################################################################################ # Begin code related to CompletionError ############################################################################################################ - def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: """Raises CompletionError""" raise CompletionError('completer broke something') - def choice_raise_error(self) -> List[str]: + def choice_raise_error(self) -> list[str]: """Raises CompletionError""" raise CompletionError('choice broke something') @@ -284,13 +276,13 @@ def do_raise_completion_error(self, args: argparse.Namespace) -> None: ############################################################################################################ # Begin code related to receiving arg_tokens ############################################################################################################ - def choices_takes_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]: + def choices_takes_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]: """Choices function that receives arg_tokens from ArgparseCompleter""" return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] def completer_takes_arg_tokens( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]] - ) -> List[str]: + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + ) -> list[str]: """Completer function that receives arg_tokens from ArgparseCompleter""" match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]] return self.basic_complete(text, line, begidx, endidx, match_against) @@ -348,13 +340,13 @@ def ac_app(): @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)) +def test_help(ac_app, command) -> None: + out1, err1 = run_cmd(ac_app, f'{command} -h') + out2, err2 = run_cmd(ac_app, f'help {command}') assert out1 == out2 -def test_bad_subcommand_help(ac_app): +def test_bad_subcommand_help(ac_app) -> None: # These should give the same output because the second one isn't using a # real subcommand, so help will be called on the music command instead. out1, err1 = run_cmd(ac_app, 'help music') @@ -363,7 +355,7 @@ def test_bad_subcommand_help(ac_app): @pytest.mark.parametrize( - 'command, text, completions', + ('command', 'text', 'completions'), [ ('', 'mus', ['music ']), ('music', 'cre', ['create ']), @@ -375,8 +367,8 @@ def test_bad_subcommand_help(ac_app): ('music fake', '', []), ], ) -def test_complete_help(ac_app, command, text, completions): - line = 'help {} {}'.format(command, text) +def test_complete_help(ac_app, command, text, completions) -> None: + line = f'help {command} {text}' endidx = len(line) begidx = endidx - len(text) @@ -390,11 +382,11 @@ def test_complete_help(ac_app, command, text, completions): @pytest.mark.parametrize( - 'subcommand, text, completions', + ('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) +def test_subcommand_completions(ac_app, subcommand, text, completions) -> None: + line = f'music {subcommand} {text}' endidx = len(line) begidx = endidx - len(text) @@ -408,7 +400,7 @@ def test_subcommand_completions(ac_app, subcommand, text, completions): @pytest.mark.parametrize( - 'command_and_args, text, completion_matches, display_matches', + ('command_and_args', 'text', 'completion_matches', 'display_matches'), [ # Complete all flags (suppressed will not show) ( @@ -538,8 +530,8 @@ def test_subcommand_completions(ac_app, subcommand, text, completions): ('pos_and_flag choice -f -h ', '', [], []), ], ) -def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matches, display_matches): - line = '{} {}'.format(command_and_args, text) +def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matches, display_matches) -> None: + line = f'{command_and_args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -549,13 +541,12 @@ 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) + assert ac_app.completion_matches == sorted(completion_matches, key=ac_app.default_sort_key) + assert ac_app.display_matches == sorted(display_matches, key=ac_app.default_sort_key) @pytest.mark.parametrize( - 'flag, text, completions', + ('flag', 'text', 'completions'), [ ('-l', '', ArgparseCompleterTester.static_choices_list), ('--list', 's', ['static', 'stop']), @@ -568,8 +559,8 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matc ('--num_completion_items', '', ArgparseCompleterTester.num_completion_items), ], ) -def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): - line = 'choices {} {}'.format(flag, text) +def test_autocomp_flag_choices_completion(ac_app, flag, text, completions) -> None: + line = f'choices {flag} {text}' endidx = len(line) begidx = endidx - len(text) @@ -590,7 +581,7 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): @pytest.mark.parametrize( - 'pos, text, completions', + ('pos', 'text', 'completions'), [ (1, '', ArgparseCompleterTester.static_choices_list), (1, 's', ['static', 'stop']), @@ -601,7 +592,7 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): (4, '', []), ], ) -def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): +def test_autocomp_positional_choices_completion(ac_app, pos, text, completions) -> None: # Generate line were preceding positionals are already filled line = 'choices {} {}'.format('foo ' * (pos - 1), text) endidx = len(line) @@ -623,7 +614,7 @@ def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): assert ac_app.completion_matches == completions -def test_flag_sorting(ac_app): +def test_flag_sorting(ac_app) -> None: # This test exercises the case where a positional arg has non-negative integers for its choices. # ArgparseCompleter will sort these numerically before converting them to strings. As a result, # cmd2.matches_sorted gets set to True. If no completion matches are returned and the entered @@ -636,20 +627,21 @@ def test_flag_sorting(ac_app): option_strings.sort(key=ac_app.default_sort_key) text = '-' - line = 'choices arg1 arg2 arg3 {}'.format(text) + line = f'choices arg1 arg2 arg3 {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, ac_app) - assert first_match is not None and ac_app.completion_matches == option_strings + assert first_match is not None + assert ac_app.completion_matches == option_strings @pytest.mark.parametrize( - 'flag, text, completions', + ('flag', 'text', 'completions'), [('-c', '', ArgparseCompleterTester.completions_for_flag), ('--completer', 'f', ['flag', 'fairly'])], ) -def test_autocomp_flag_completers(ac_app, flag, text, completions): - line = 'completer {} {}'.format(flag, text) +def test_autocomp_flag_completers(ac_app, flag, text, completions) -> None: + line = f'completer {flag} {text}' endidx = len(line) begidx = endidx - len(text) @@ -663,7 +655,7 @@ def test_autocomp_flag_completers(ac_app, flag, text, completions): @pytest.mark.parametrize( - 'pos, text, completions', + ('pos', 'text', 'completions'), [ (1, '', ArgparseCompleterTester.completions_for_pos_1), (1, 'p', ['positional_1', 'probably']), @@ -671,7 +663,7 @@ def test_autocomp_flag_completers(ac_app, flag, text, completions): (2, 'm', ['missed', 'me']), ], ) -def test_autocomp_positional_completers(ac_app, pos, text, completions): +def test_autocomp_positional_completers(ac_app, pos, text, completions) -> None: # Generate line were preceding positionals are already filled line = 'completer {} {}'.format('foo ' * (pos - 1), text) endidx = len(line) @@ -686,7 +678,7 @@ def test_autocomp_positional_completers(ac_app, pos, text, completions): assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) -def test_autocomp_blank_token(ac_app): +def test_autocomp_blank_token(ac_app) -> None: """Force a blank token to make sure ArgparseCompleter consumes them like argparse does""" from cmd2.argparse_completer import ( ArgparseCompleter, @@ -696,7 +688,7 @@ def test_autocomp_blank_token(ac_app): # Blank flag arg will be consumed. Therefore we expect to be completing the first positional. text = '' - line = 'completer -c {} {}'.format(blank, text) + line = f'completer -c {blank} {text}' endidx = len(line) begidx = endidx - len(text) @@ -707,7 +699,7 @@ def test_autocomp_blank_token(ac_app): # Blank arg for first positional will be consumed. Therefore we expect to be completing the second positional. text = '' - line = 'completer {} {}'.format(blank, text) + line = f'completer {blank} {text}' endidx = len(line) begidx = endidx - len(text) @@ -717,10 +709,10 @@ def test_autocomp_blank_token(ac_app): assert sorted(completions) == sorted(ArgparseCompleterTester.completions_for_pos_2) -def test_completion_items(ac_app): +def test_completion_items(ac_app) -> None: # First test CompletionItems created from strings text = '' - line = 'choices --completion_items {}'.format(text) + line = f'choices --completion_items {text}' endidx = len(line) begidx = endidx - len(text) @@ -742,7 +734,7 @@ def test_completion_items(ac_app): # Now test CompletionItems created from numbers text = '' - line = 'choices --num_completion_items {}'.format(text) + line = f'choices --num_completion_items {text}' endidx = len(line) begidx = endidx - len(text) @@ -765,7 +757,7 @@ def test_completion_items(ac_app): @pytest.mark.parametrize( - 'num_aliases, show_description', + ('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, @@ -775,15 +767,15 @@ def test_completion_items(ac_app): (100, False), ], ) -def test_max_completion_items(ac_app, num_aliases, show_description): +def test_max_completion_items(ac_app, num_aliases, show_description) -> None: # Create aliases - for i in range(0, num_aliases): - run_cmd(ac_app, 'alias create fake_alias{} help'.format(i)) + for i in range(num_aliases): + run_cmd(ac_app, f'alias create fake_alias{i} help') assert len(ac_app.aliases) == num_aliases text = 'fake_alias' - line = 'alias list {}'.format(text) + line = f'alias list {text}' endidx = len(line) begidx = endidx - len(text) @@ -805,7 +797,7 @@ def test_max_completion_items(ac_app, num_aliases, show_description): @pytest.mark.parametrize( - 'args, completions', + ('args', 'completions'), [ # Flag with nargs = 2 ('--set_value', ArgparseCompleterTester.set_value_choices), @@ -855,9 +847,9 @@ def test_max_completion_items(ac_app, num_aliases, show_description): ('the positional remainder --set_value', ['choices ']), ], ) -def test_autcomp_nargs(ac_app, args, completions): +def test_autcomp_nargs(ac_app, args, completions) -> None: text = '' - line = 'nargs {} {}'.format(args, text) + line = f'nargs {args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -871,7 +863,7 @@ def test_autcomp_nargs(ac_app, args, completions): @pytest.mark.parametrize( - 'command_and_args, text, is_error', + ('command_and_args', 'text', 'is_error'), [ # Flag is finished before moving on ('hint --flag foo --', '', False), @@ -903,8 +895,8 @@ def test_autcomp_nargs(ac_app, args, completions): ('nargs --range', '--', True), ], ) -def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys): - line = '{} {}'.format(command_and_args, text) +def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys) -> None: + line = f'{command_and_args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -914,10 +906,10 @@ def test_unfinished_flag_error(ac_app, command_and_args, text, is_error, capsys) assert is_error == all(x in out for x in ["Error: argument", "expected"]) -def test_completion_items_arg_header(ac_app): +def test_completion_items_arg_header(ac_app) -> None: # Test when metavar is None text = '' - line = 'choices --desc_header {}'.format(text) + line = f'choices --desc_header {text}' endidx = len(line) begidx = endidx - len(text) @@ -926,7 +918,7 @@ def test_completion_items_arg_header(ac_app): # Test when metavar is a string text = '' - line = 'choices --no_header {}'.format(text) + line = f'choices --no_header {text}' endidx = len(line) begidx = endidx - len(text) @@ -935,7 +927,7 @@ def test_completion_items_arg_header(ac_app): # Test when metavar is a tuple text = '' - line = 'choices --tuple_metavar {}'.format(text) + line = f'choices --tuple_metavar {text}' endidx = len(line) begidx = endidx - len(text) @@ -944,7 +936,7 @@ def test_completion_items_arg_header(ac_app): assert ac_app.TUPLE_METAVAR[0].upper() in normalize(ac_app.formatted_completions)[0] text = '' - line = 'choices --tuple_metavar token_1 {}'.format(text) + line = f'choices --tuple_metavar token_1 {text}' endidx = len(line) begidx = endidx - len(text) @@ -953,7 +945,7 @@ def test_completion_items_arg_header(ac_app): assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0] text = '' - line = 'choices --tuple_metavar token_1 token_2 {}'.format(text) + line = f'choices --tuple_metavar token_1 token_2 {text}' endidx = len(line) begidx = endidx - len(text) @@ -963,14 +955,14 @@ def test_completion_items_arg_header(ac_app): assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0] -def test_completion_items_descriptive_header(ac_app): +def test_completion_items_descriptive_header(ac_app) -> None: from cmd2.argparse_completer import ( DEFAULT_DESCRIPTIVE_HEADER, ) # This argument provided a descriptive header text = '' - line = 'choices --desc_header {}'.format(text) + line = f'choices --desc_header {text}' endidx = len(line) begidx = endidx - len(text) @@ -979,7 +971,7 @@ def test_completion_items_descriptive_header(ac_app): # This argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADER text = '' - line = 'choices --no_header {}'.format(text) + line = f'choices --no_header {text}' endidx = len(line) begidx = endidx - len(text) @@ -988,7 +980,7 @@ def test_completion_items_descriptive_header(ac_app): @pytest.mark.parametrize( - 'command_and_args, text, has_hint', + ('command_and_args', 'text', 'has_hint'), [ # Normal cases ('hint', '', True), @@ -1013,8 +1005,8 @@ def test_completion_items_descriptive_header(ac_app): ('nargs the choices remainder', '-', True), ], ) -def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys): - line = '{} {}'.format(command_and_args, text) +def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys) -> None: + line = f'{command_and_args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1026,9 +1018,9 @@ def test_autocomp_hint(ac_app, command_and_args, text, has_hint, capsys): assert not out -def test_autocomp_hint_no_help_text(ac_app, capsys): +def test_autocomp_hint_no_help_text(ac_app, capsys) -> None: text = '' - line = 'hint foo {}'.format(text) + line = f'hint foo {text}' endidx = len(line) begidx = endidx - len(text) @@ -1036,18 +1028,11 @@ def test_autocomp_hint_no_help_text(ac_app, capsys): out, err = capsys.readouterr() assert first_match is None - assert ( - not out - == ''' -Hint: - NO_HELP_POS - -''' - ) + assert out != '''\nHint:\n NO_HELP_POS\n\n''' @pytest.mark.parametrize( - 'args, text', + ('args', 'text'), [ # Exercise a flag arg and choices function that raises a CompletionError ('--choice ', 'choice'), @@ -1055,8 +1040,8 @@ def test_autocomp_hint_no_help_text(ac_app, capsys): ('', 'completer'), ], ) -def test_completion_error(ac_app, capsys, args, text): - line = 'raise_completion_error {} {}'.format(args, text) +def test_completion_error(ac_app, capsys, args, text) -> None: + line = f'raise_completion_error {args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1064,11 +1049,11 @@ def test_completion_error(ac_app, capsys, args, text): out, err = capsys.readouterr() assert first_match is None - assert "{} broke something".format(text) in out + assert f"{text} broke something" in out @pytest.mark.parametrize( - 'command_and_args, completions', + ('command_and_args', 'completions'), [ # Exercise a choices function that receives arg_tokens dictionary ('arg_tokens choice subcmd', ['choice', 'subcmd']), @@ -1078,9 +1063,9 @@ def test_completion_error(ac_app, capsys, args, text): ('arg_tokens completer subcmd --parent_arg override fake', ['override', 'subcmd']), ], ) -def test_arg_tokens(ac_app, command_and_args, completions): +def test_arg_tokens(ac_app, command_and_args, completions) -> None: text = '' - line = '{} {}'.format(command_and_args, text) + line = f'{command_and_args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1094,7 +1079,7 @@ def test_arg_tokens(ac_app, command_and_args, completions): @pytest.mark.parametrize( - 'command_and_args, text, output_contains, first_match', + ('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), @@ -1116,8 +1101,8 @@ def test_arg_tokens(ac_app, command_and_args, completions): ('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) +def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, first_match, capsys) -> None: + line = f'{command_and_args} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1127,7 +1112,7 @@ def test_complete_mutex_group(ac_app, command_and_args, text, output_contains, f assert output_contains in out -def test_single_prefix_char(): +def test_single_prefix_char() -> None: from cmd2.argparse_completer import ( _single_prefix_char, ) @@ -1146,7 +1131,7 @@ def test_single_prefix_char(): assert _single_prefix_char('+', parser) -def test_looks_like_flag(): +def test_looks_like_flag() -> None: from cmd2.argparse_completer import ( _looks_like_flag, ) @@ -1166,7 +1151,7 @@ def test_looks_like_flag(): assert _looks_like_flag('--flag', parser) -def test_complete_command_no_tokens(ac_app): +def test_complete_command_no_tokens(ac_app) -> None: from cmd2.argparse_completer import ( ArgparseCompleter, ) @@ -1178,7 +1163,7 @@ def test_complete_command_no_tokens(ac_app): assert not completions -def test_complete_command_help_no_tokens(ac_app): +def test_complete_command_help_no_tokens(ac_app) -> None: from cmd2.argparse_completer import ( ArgparseCompleter, ) @@ -1190,10 +1175,12 @@ def test_complete_command_help_no_tokens(ac_app): assert not completions -@pytest.mark.parametrize('flag, completions', [('--provider', standalone_choices), ('--completer', standalone_completions)]) -def test_complete_standalone(ac_app, flag, completions): +@pytest.mark.parametrize( + ('flag', 'completions'), [('--provider', standalone_choices), ('--completer', standalone_completions)] +) +def test_complete_standalone(ac_app, flag, completions) -> None: text = '' - line = 'standalone {} {}'.format(flag, text) + line = f'standalone {flag} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1204,9 +1191,8 @@ def test_complete_standalone(ac_app, flag, completions): # Custom ArgparseCompleter-based class class CustomCompleter(argparse_completer.ArgparseCompleter): - def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]: + def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]: """Override so flags with 'complete_when_ready' set to True will complete only when app is ready""" - # Find flags which should not be completed and place them in matched_flags for flag in self._flags: action = self._flag_to_action[flag] @@ -1214,7 +1200,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche if action.get_complete_when_ready() is True and not app.is_ready: matched_flags.append(flag) - return super(CustomCompleter, self)._complete_flags(text, line, begidx, endidx, matched_flags) + return super()._complete_flags(text, line, begidx, endidx, matched_flags) # Add a custom argparse action attribute @@ -1223,7 +1209,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche # App used to test custom ArgparseCompleter types and custom argparse attributes class CustomCompleterApp(cmd2.Cmd): - def __init__(self): + def __init__(self) -> None: super().__init__() self.is_ready = True @@ -1234,7 +1220,6 @@ def __init__(self): @with_argparser(default_completer_parser) def do_default_completer(self, args: argparse.Namespace) -> None: """Test command""" - pass # Parser that's used to test setting a custom completer at the parser level custom_completer_parser = Cmd2ArgumentParser( @@ -1245,7 +1230,6 @@ def do_default_completer(self, args: argparse.Namespace) -> None: @with_argparser(custom_completer_parser) def do_custom_completer(self, args: argparse.Namespace) -> None: """Test command""" - pass # Test as_subcommand_to decorator with custom completer top_parser = Cmd2ArgumentParser(description="Top Command") @@ -1267,21 +1251,20 @@ def _subcmd_no_custom(self, args: argparse.Namespace) -> None: pass # Parser for a subcommand with a custom completer type - custom_completer_parser = Cmd2ArgumentParser(description="Custom completer", ap_completer_type=CustomCompleter) - custom_completer_parser.add_argument('--myflag', complete_when_ready=True) + custom_completer_parser2 = Cmd2ArgumentParser(description="Custom completer", ap_completer_type=CustomCompleter) + custom_completer_parser2.add_argument('--myflag', complete_when_ready=True) - @cmd2.as_subcommand_to('top', 'custom', custom_completer_parser, help="custom completer") + @cmd2.as_subcommand_to('top', 'custom', custom_completer_parser2, help="custom completer") def _subcmd_custom(self, args: argparse.Namespace) -> None: pass @pytest.fixture def custom_completer_app(): - app = CustomCompleterApp() - return app + return CustomCompleterApp() -def test_default_custom_completer_type(custom_completer_app: CustomCompleterApp): +def test_default_custom_completer_type(custom_completer_app: CustomCompleterApp) -> None: """Test altering the app-wide default ArgparseCompleter type""" try: argparse_completer.set_default_ap_completer_type(CustomCompleter) @@ -1306,7 +1289,7 @@ def test_default_custom_completer_type(custom_completer_app: CustomCompleterApp) argparse_completer.set_default_ap_completer_type(argparse_completer.ArgparseCompleter) -def test_custom_completer_type(custom_completer_app: CustomCompleterApp): +def test_custom_completer_type(custom_completer_app: CustomCompleterApp) -> None: """Test parser with a specific custom ArgparseCompleter type""" text = '--m' line = f'custom_completer {text}' @@ -1324,9 +1307,8 @@ def test_custom_completer_type(custom_completer_app: CustomCompleterApp): assert not custom_completer_app.completion_matches -def test_decorated_subcmd_custom_completer(custom_completer_app: CustomCompleterApp): +def test_decorated_subcmd_custom_completer(custom_completer_app: CustomCompleterApp) -> None: """Tests custom completer type on a subcommand created with @cmd2.as_subcommand_to""" - # First test the subcommand without the custom completer text = '--m' line = f'top no_custom {text}' @@ -1359,7 +1341,7 @@ def test_decorated_subcmd_custom_completer(custom_completer_app: CustomCompleter assert not custom_completer_app.completion_matches -def test_add_parser_custom_completer(): +def test_add_parser_custom_completer() -> None: """Tests setting a custom completer type on a subcommand using add_parser()""" parser = Cmd2ArgumentParser() subparsers = parser.add_subparsers() diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index 06d7b7d3d..bd79910e3 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -1,7 +1,4 @@ -# flake8: noqa E302 -""" -Unit/functional testing for argparse customizations in cmd2 -""" +"""Unit/functional testing for argparse customizations in cmd2""" import argparse @@ -24,7 +21,7 @@ class ApCustomTestApp(cmd2.Cmd): """Test app for cmd2's argparse customization""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) range_parser = Cmd2ArgumentParser() @@ -36,7 +33,7 @@ def __init__(self, *args, **kwargs): range_parser.add_argument('--arg5', nargs=argparse.ONE_OR_MORE) @cmd2.with_argparser(range_parser) - def do_range(self, _): + def do_range(self, _) -> None: pass @@ -45,58 +42,58 @@ def cust_app(): return ApCustomTestApp() -def fake_func(): +def fake_func() -> None: pass @pytest.mark.parametrize( - 'kwargs, is_valid', + ('kwargs', 'is_valid'), [ ({'choices_provider': fake_func}, True), ({'completer': fake_func}, True), ({'choices_provider': fake_func, 'completer': fake_func}, False), ], ) -def test_apcustom_choices_callable_count(kwargs, is_valid): +def test_apcustom_choices_callable_count(kwargs, is_valid) -> None: parser = Cmd2ArgumentParser() - try: + if is_valid: parser.add_argument('name', **kwargs) - assert is_valid - except ValueError as ex: - assert not is_valid - assert 'Only one of the following parameters' in str(ex) + else: + expected_err = 'Only one of the following parameters' + with pytest.raises(ValueError, match=expected_err): + parser.add_argument('name', **kwargs) @pytest.mark.parametrize('kwargs', [({'choices_provider': fake_func}), ({'completer': fake_func})]) -def test_apcustom_no_choices_callables_alongside_choices(kwargs): +def test_apcustom_no_choices_callables_alongside_choices(kwargs) -> None: + parser = Cmd2ArgumentParser() with pytest.raises(TypeError) as excinfo: - parser = Cmd2ArgumentParser() parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs) assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value) @pytest.mark.parametrize('kwargs', [({'choices_provider': fake_func}), ({'completer': fake_func})]) -def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs): +def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs) -> None: + parser = Cmd2ArgumentParser() with pytest.raises(TypeError) as excinfo: - parser = Cmd2ArgumentParser() parser.add_argument('--name', action='store_true', **kwargs) assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value) -def test_apcustom_usage(): +def test_apcustom_usage() -> None: usage = "A custom usage statement" parser = Cmd2ArgumentParser(usage=usage) assert usage in parser.format_help() -def test_apcustom_nargs_help_format(cust_app): +def test_apcustom_nargs_help_format(cust_app) -> None: out, err = run_cmd(cust_app, 'help range') assert 'Usage: range [-h] [--arg0 ARG0] [--arg1 ARG1{2}] [--arg2 ARG2{3+}]' in out[0] assert ' [--arg3 ARG3{2..3}] [--arg4 [ARG4 [...]]] [--arg5 ARG5 [...]]' in out[1] -def test_apcustom_nargs_range_validation(cust_app): - # nargs = (3,) +def test_apcustom_nargs_range_validation(cust_app) -> None: + # nargs = (3,) # noqa: ERA001 out, err = run_cmd(cust_app, 'range --arg2 one two') assert 'Error: argument --arg2: expected at least 3 arguments' in err[2] @@ -106,7 +103,7 @@ def test_apcustom_nargs_range_validation(cust_app): out, err = run_cmd(cust_app, 'range --arg2 one two three four') assert not err - # nargs = (2,3) + # nargs = (2,3) # noqa: ERA001 out, err = run_cmd(cust_app, 'range --arg3 one') assert 'Error: argument --arg3: expected 2 to 3 arguments' in err[2] @@ -126,28 +123,28 @@ def test_apcustom_nargs_range_validation(cust_app): (1, 2, 3), ], ) -def test_apcustom_narg_invalid_tuples(nargs_tuple): - with pytest.raises(ValueError) as excinfo: - parser = Cmd2ArgumentParser() +def test_apcustom_narg_invalid_tuples(nargs_tuple) -> None: + parser = Cmd2ArgumentParser() + expected_err = 'Ranged values for nargs must be a tuple of 1 or 2 integers' + with pytest.raises(ValueError, match=expected_err): parser.add_argument('invalid_tuple', nargs=nargs_tuple) - assert 'Ranged values for nargs must be a tuple of 1 or 2 integers' in str(excinfo.value) -def test_apcustom_narg_tuple_order(): - with pytest.raises(ValueError) as excinfo: - parser = Cmd2ArgumentParser() +def test_apcustom_narg_tuple_order() -> None: + parser = Cmd2ArgumentParser() + expected_err = 'Invalid nargs range. The first value must be less than the second' + with pytest.raises(ValueError, match=expected_err): parser.add_argument('invalid_tuple', nargs=(2, 1)) - assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value) -def test_apcustom_narg_tuple_negative(): - with pytest.raises(ValueError) as excinfo: - parser = Cmd2ArgumentParser() +def test_apcustom_narg_tuple_negative() -> None: + parser = Cmd2ArgumentParser() + expected_err = 'Negative numbers are invalid for nargs range' + with pytest.raises(ValueError, match=expected_err): parser.add_argument('invalid_tuple', nargs=(-1, 1)) - assert 'Negative numbers are invalid for nargs range' in str(excinfo.value) -def test_apcustom_narg_tuple_zero_base(): +def test_apcustom_narg_tuple_zero_base() -> None: parser = Cmd2ArgumentParser() arg = parser.add_argument('arg', nargs=(0,)) assert arg.nargs == argparse.ZERO_OR_MORE @@ -167,7 +164,7 @@ def test_apcustom_narg_tuple_zero_base(): assert "arg{0..3}" in parser.format_help() -def test_apcustom_narg_tuple_one_base(): +def test_apcustom_narg_tuple_one_base() -> None: parser = Cmd2ArgumentParser() arg = parser.add_argument('arg', nargs=(1,)) assert arg.nargs == argparse.ONE_OR_MORE @@ -181,7 +178,7 @@ def test_apcustom_narg_tuple_one_base(): assert "arg{1..5}" in parser.format_help() -def test_apcustom_narg_tuple_other_ranges(): +def test_apcustom_narg_tuple_other_ranges() -> None: # Test range with no upper bound on max parser = Cmd2ArgumentParser() arg = parser.add_argument('arg', nargs=(2,)) @@ -195,7 +192,7 @@ def test_apcustom_narg_tuple_other_ranges(): assert arg.nargs_range == (2, 5) -def test_apcustom_print_message(capsys): +def test_apcustom_print_message(capsys) -> None: import sys test_message = 'The test message' @@ -213,7 +210,7 @@ def test_apcustom_print_message(capsys): assert test_message in err -def test_generate_range_error(): +def test_generate_range_error() -> None: # max is INFINITY err_str = generate_range_error(1, constants.INFINITY) assert err_str == "expected at least 1 argument" @@ -236,14 +233,14 @@ def test_generate_range_error(): assert err_str == "expected 0 to 2 arguments" -def test_apcustom_required_options(): +def test_apcustom_required_options() -> None: # Make sure a 'required arguments' section shows when a flag is marked required parser = Cmd2ArgumentParser() parser.add_argument('--required_flag', required=True) assert 'required arguments' in parser.format_help() -def test_override_parser(): +def test_override_parser() -> None: """Test overriding argparse_custom.DEFAULT_ARGUMENT_PARSER""" import importlib @@ -252,7 +249,7 @@ def test_override_parser(): ) # The standard parser is Cmd2ArgumentParser - assert argparse_custom.DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser + assert Cmd2ArgumentParser == argparse_custom.DEFAULT_ARGUMENT_PARSER # Set our parser module and force a reload of cmd2 so it loads the module argparse.cmd2_parser_module = 'examples.custom_parser' @@ -263,17 +260,17 @@ def test_override_parser(): CustomParser, ) - assert argparse_custom.DEFAULT_ARGUMENT_PARSER == CustomParser + assert CustomParser == argparse_custom.DEFAULT_ARGUMENT_PARSER -def test_apcustom_metavar_tuple(): +def test_apcustom_metavar_tuple() -> None: # Test the case when a tuple metavar is used with nargs an integer > 1 parser = Cmd2ArgumentParser() parser.add_argument('--aflag', nargs=2, metavar=('foo', 'bar'), help='This is a test') assert '[--aflag foo bar]' in parser.format_help() -def test_cmd2_attribute_wrapper(): +def test_cmd2_attribute_wrapper() -> None: initial_val = 5 wrapper = cmd2.Cmd2AttributeWrapper(initial_val) assert wrapper.get() == initial_val @@ -283,9 +280,8 @@ def test_cmd2_attribute_wrapper(): assert wrapper.get() == new_val -def test_completion_items_as_choices(capsys): - """ - Test cmd2's patch to Argparse._check_value() which supports CompletionItems as choices. +def test_completion_items_as_choices(capsys) -> None: + """Test cmd2's patch to Argparse._check_value() which supports CompletionItems as choices. Choices are compared to CompletionItems.orig_value instead of the CompletionItem instance. """ from cmd2.argparse_custom import ( diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py old mode 100755 new mode 100644 index 721be0a2f..2f9073f1f --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Cmd2 unit/functional testing -""" +"""Cmd2 unit/functional testing""" import builtins import io @@ -13,6 +9,7 @@ from code import ( InteractiveConsole, ) +from typing import NoReturn from unittest import ( mock, ) @@ -64,7 +61,7 @@ def cmd_wrapper(*args, **kwargs): return arg_decorator -def CreateOutsimApp(): +def create_outsim_app(): c = cmd2.Cmd() c.stdout = utils.StdSim(c.stdout) return c @@ -72,14 +69,14 @@ def CreateOutsimApp(): @pytest.fixture def outsim_app(): - return CreateOutsimApp() + return create_outsim_app() -def test_version(base_app): +def test_version(base_app) -> None: assert cmd2.__version__ -def test_not_in_main_thread(base_app, capsys): +def test_not_in_main_thread(base_app, capsys) -> None: import threading # Mock threading.main_thread() to return our fake thread @@ -95,19 +92,19 @@ def test_not_in_main_thread(base_app, capsys): assert "cmdloop must be run in the main thread" in str(excinfo.value) -def test_empty_statement(base_app): +def test_empty_statement(base_app) -> None: out, err = run_cmd(base_app, '') expected = normalize('') assert out == expected -def test_base_help(base_app): +def test_base_help(base_app) -> None: out, err = run_cmd(base_app, 'help') assert base_app.last_result is True verify_help_text(base_app, out) -def test_base_help_verbose(base_app): +def test_base_help_verbose(base_app) -> None: out, err = run_cmd(base_app, 'help -v') assert base_app.last_result is True verify_help_text(base_app, out) @@ -123,7 +120,7 @@ def test_base_help_verbose(base_app): assert ':param' not in ''.join(out) -def test_base_argparse_help(base_app): +def test_base_argparse_help(base_app) -> None: # 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') out2, err2 = run_cmd(base_app, 'help set') @@ -134,26 +131,26 @@ def test_base_argparse_help(base_app): assert out1[2].startswith('Set a settable parameter') -def test_base_invalid_option(base_app): +def test_base_invalid_option(base_app) -> None: out, err = run_cmd(base_app, 'set -z') assert err[0] == 'Usage: set [-h] [param] [value]' assert 'Error: unrecognized arguments: -z' in err[1] -def test_base_shortcuts(base_app): +def test_base_shortcuts(base_app) -> None: out, err = run_cmd(base_app, 'shortcuts') expected = normalize(SHORTCUTS_TXT) assert out == expected assert base_app.last_result is True -def test_command_starts_with_shortcut(): - with pytest.raises(ValueError) as excinfo: +def test_command_starts_with_shortcut() -> None: + expected_err = "Invalid command name 'help'" + with pytest.raises(ValueError, match=expected_err): cmd2.Cmd(shortcuts={'help': 'fake'}) - assert "Invalid command name 'help'" in str(excinfo.value) -def test_base_set(base_app): +def test_base_set(base_app) -> None: # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out, err = run_cmd(base_app, 'set') @@ -165,7 +162,7 @@ def test_base_set(base_app): assert base_app.last_result[param] == base_app.settables[param].get_value() -def test_set(base_app): +def test_set(base_app) -> None: out, err = run_cmd(base_app, 'set quiet True') expected = normalize( """ @@ -189,21 +186,21 @@ def test_set(base_app): assert base_app.last_result['quiet'] is True -def test_set_val_empty(base_app): +def test_set_val_empty(base_app) -> None: base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor ""') assert base_app.editor == '' assert base_app.last_result is True -def test_set_val_is_flag(base_app): +def test_set_val_is_flag(base_app) -> None: base_app.editor = "fake" out, err = run_cmd(base_app, 'set editor "-h"') assert base_app.editor == '-h' assert base_app.last_result is True -def test_set_not_supported(base_app): +def test_set_not_supported(base_app) -> None: out, err = run_cmd(base_app, 'set qqq True') expected = normalize( """ @@ -214,7 +211,7 @@ def test_set_not_supported(base_app): assert base_app.last_result is False -def test_set_no_settables(base_app): +def test_set_no_settables(base_app) -> None: base_app._settables.clear() out, err = run_cmd(base_app, 'set quiet True') expected = normalize("There are no settable parameters") @@ -223,7 +220,7 @@ def test_set_no_settables(base_app): @pytest.mark.parametrize( - 'new_val, is_valid, expected', + ('new_val', 'is_valid', 'expected'), [ (ansi.AllowStyle.NEVER, True, ansi.AllowStyle.NEVER), ('neVeR', True, ansi.AllowStyle.NEVER), @@ -234,12 +231,12 @@ def test_set_no_settables(base_app): ('invalid', False, ansi.AllowStyle.TERMINAL), ], ) -def test_set_allow_style(base_app, new_val, is_valid, expected): +def test_set_allow_style(base_app, new_val, is_valid, expected) -> None: # Initialize allow_style for this test ansi.allow_style = ansi.AllowStyle.TERMINAL # Use the set command to alter it - out, err = run_cmd(base_app, 'set allow_style {}'.format(new_val)) + out, err = run_cmd(base_app, f'set allow_style {new_val}') assert base_app.last_result is is_valid # Verify the results @@ -252,7 +249,7 @@ def test_set_allow_style(base_app, new_val, is_valid, expected): ansi.allow_style = ansi.AllowStyle.TERMINAL -def test_set_with_choices(base_app): +def test_set_with_choices(base_app) -> None: """Test choices validation of Settables""" fake_choices = ['valid', 'choices'] base_app.fake = fake_choices[0] @@ -272,7 +269,7 @@ def test_set_with_choices(base_app): class OnChangeHookApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.add_settable(utils.Settable('quiet', bool, "my description", self, onchange_cb=self._onchange_quiet)) @@ -283,11 +280,10 @@ def _onchange_quiet(self, name, old, new) -> None: @pytest.fixture def onchange_app(): - app = OnChangeHookApp() - return app + return OnChangeHookApp() -def test_set_onchange_hook(onchange_app): +def test_set_onchange_hook(onchange_app) -> None: out, err = run_cmd(onchange_app, 'set quiet True') expected = normalize( """ @@ -300,7 +296,7 @@ def test_set_onchange_hook(onchange_app): assert onchange_app.last_result is True -def test_base_shell(base_app, monkeypatch): +def test_base_shell(base_app, monkeypatch) -> None: m = mock.Mock() monkeypatch.setattr("{}.Popen".format('subprocess'), m) out, err = run_cmd(base_app, 'shell echo a') @@ -308,13 +304,13 @@ def test_base_shell(base_app, monkeypatch): assert m.called -def test_shell_last_result(base_app): +def test_shell_last_result(base_app) -> None: base_app.last_result = None run_cmd(base_app, 'shell fake') assert base_app.last_result is not None -def test_shell_manual_call(base_app): +def test_shell_manual_call(base_app) -> None: # Verifies crash from Issue #986 doesn't happen cmds = ['echo "hi"', 'echo "there"', 'echo "cmd2!"'] cmd = ';'.join(cmds) @@ -326,12 +322,12 @@ def test_shell_manual_call(base_app): base_app.do_shell(cmd) -def test_base_error(base_app): +def test_base_error(base_app) -> None: out, err = run_cmd(base_app, 'meow') assert "is not a recognized command" in err[0] -def test_base_error_suggest_command(base_app): +def test_base_error_suggest_command(base_app) -> None: try: old_suggest_similar_command = base_app.suggest_similar_command base_app.suggest_similar_command = True @@ -341,7 +337,7 @@ def test_base_error_suggest_command(base_app): base_app.suggest_similar_command = old_suggest_similar_command -def test_run_script(base_app, request): +def test_run_script(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') @@ -349,7 +345,7 @@ def test_run_script(base_app, request): assert base_app._current_script_dir is None # Get output out the script - script_out, script_err = run_cmd(base_app, 'run_script {}'.format(filename)) + script_out, script_err = run_cmd(base_app, f'run_script {filename}') assert base_app.last_result is True assert base_app._script_dir == [] @@ -370,13 +366,13 @@ def test_run_script(base_app, request): assert script_err == manual_err -def test_run_script_with_empty_args(base_app): +def test_run_script_with_empty_args(base_app) -> None: out, err = run_cmd(base_app, 'run_script') assert "the following arguments are required" in err[1] assert base_app.last_result is None -def test_run_script_with_invalid_file(base_app, request): +def test_run_script_with_invalid_file(base_app, request) -> None: # Path does not exist out, err = run_cmd(base_app, 'run_script does_not_exist.txt') assert "Problem accessing script from " in err[0] @@ -384,39 +380,40 @@ def test_run_script_with_invalid_file(base_app, request): # Path is a directory test_dir = os.path.dirname(request.module.__file__) - out, err = run_cmd(base_app, 'run_script {}'.format(test_dir)) + out, err = run_cmd(base_app, f'run_script {test_dir}') assert "Problem accessing script from " in err[0] assert base_app.last_result is False -def test_run_script_with_empty_file(base_app, request): +def test_run_script_with_empty_file(base_app, request) -> None: 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 + out, err = run_cmd(base_app, f'run_script {filename}') + assert not out + assert not err assert base_app.last_result is True -def test_run_script_with_binary_file(base_app, request): +def test_run_script_with_binary_file(base_app, request) -> None: 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)) + out, err = run_cmd(base_app, f'run_script {filename}') assert "is not an ASCII or UTF-8 encoded text file" in err[0] assert base_app.last_result is False -def test_run_script_with_python_file(base_app, request): +def test_run_script_with_python_file(base_app, request) -> None: m = mock.MagicMock(name='input', return_value='2') builtins.input = m test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'pyscript', 'stop.py') - out, err = run_cmd(base_app, 'run_script {}'.format(filename)) + out, err = run_cmd(base_app, f'run_script {filename}') assert "appears to be a Python file" in err[0] assert base_app.last_result is False -def test_run_script_with_utf8_file(base_app, request): +def test_run_script_with_utf8_file(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') @@ -424,7 +421,7 @@ def test_run_script_with_utf8_file(base_app, request): assert base_app._current_script_dir is None # Get output out the script - script_out, script_err = run_cmd(base_app, 'run_script {}'.format(filename)) + script_out, script_err = run_cmd(base_app, f'run_script {filename}') assert base_app.last_result is True assert base_app._script_dir == [] @@ -445,7 +442,7 @@ def test_run_script_with_utf8_file(base_app, request): assert script_err == manual_err -def test_scripts_add_to_history(base_app, request): +def test_scripts_add_to_history(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'help.txt') command = f'run_script {filename}' @@ -466,7 +463,7 @@ def test_scripts_add_to_history(base_app, request): assert base_app.history.get(1).raw == command -def test_run_script_nested_run_scripts(base_app, request): +def test_run_script_nested_run_scripts(base_app, request) -> None: # Verify that running a script with nested run_script commands works correctly, # and runs the nested script commands in the correct order. test_dir = os.path.dirname(request.module.__file__) @@ -478,47 +475,41 @@ def test_run_script_nested_run_scripts(base_app, request): assert base_app.last_result is True # Check that the right commands were executed. - expected = ( - """ -%s + expected = f""" +{initial_run} _relative_run_script precmds.txt set allow_style Always help shortcuts _relative_run_script postcmds.txt 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): +def test_runcmds_plus_hooks(base_app, request) -> None: 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]) - expected = """ -run_script %s + expected = f""" +run_script {prefilepath} set allow_style Always help shortcuts -run_script %s -set allow_style Never""" % ( - prefilepath, - postfilepath, - ) +run_script {postfilepath} +set allow_style Never""" out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) -def test_runcmds_plus_hooks_ctrl_c(base_app, capsys): +def test_runcmds_plus_hooks_ctrl_c(base_app, capsys) -> None: """Test Ctrl-C while in runcmds_plus_hooks""" import types - def do_keyboard_interrupt(self, _): + def do_keyboard_interrupt(self, _) -> NoReturn: raise KeyboardInterrupt('Interrupting this command') setattr(base_app, 'do_keyboard_interrupt', types.MethodType(do_keyboard_interrupt, base_app)) @@ -538,7 +529,7 @@ def do_keyboard_interrupt(self, _): assert len(base_app.history) == 2 -def test_relative_run_script(base_app, request): +def test_relative_run_script(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') @@ -546,7 +537,7 @@ def test_relative_run_script(base_app, request): assert base_app._current_script_dir is None # Get output out the script - script_out, script_err = run_cmd(base_app, '_relative_run_script {}'.format(filename)) + script_out, script_err = run_cmd(base_app, f'_relative_run_script {filename}') assert base_app.last_result is True assert base_app._script_dir == [] @@ -568,25 +559,25 @@ def test_relative_run_script(base_app, request): @pytest.mark.parametrize('file_name', odd_file_names) -def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch): +def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch) -> None: """Test file names with various patterns""" # Mock out the do_run_script call to see what args are passed to it run_script_mock = mock.MagicMock(name='do_run_script') monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock) - run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name))) + run_cmd(base_app, f"_relative_run_script {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): +def test_relative_run_script_requires_an_argument(base_app) -> None: out, err = run_cmd(base_app, '_relative_run_script') assert 'Error: the following arguments' in err[1] assert base_app.last_result is None -def test_in_script(request): +def test_in_script(request) -> None: class HookApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.register_cmdfinalization_hook(self.hook) @@ -598,18 +589,18 @@ def hook(self: cmd2.Cmd, data: plugin.CommandFinalizationData) -> plugin.Command hook_app = HookApp() test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - out, err = run_cmd(hook_app, 'run_script {}'.format(filename)) + out, err = run_cmd(hook_app, f'run_script {filename}') assert "WE ARE IN SCRIPT" in out[-1] -def test_system_exit_in_command(base_app, capsys): +def test_system_exit_in_command(base_app, capsys) -> None: """Test raising SystemExit in a command""" import types exit_code = 5 - def do_system_exit(self, _): + def do_system_exit(self, _) -> NoReturn: raise SystemExit(exit_code) setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app)) @@ -619,34 +610,35 @@ def do_system_exit(self, _): assert base_app.exit_code == exit_code -def test_passthrough_exception_in_command(base_app): +def test_passthrough_exception_in_command(base_app) -> None: """Test raising a PassThroughException in a command""" import types - def do_passthrough(self, _): - wrapped_ex = OSError("Pass me up") + expected_err = "Pass me up" + + def do_passthrough(self, _) -> NoReturn: + wrapped_ex = OSError(expected_err) raise exceptions.PassThroughException(wrapped_ex=wrapped_ex) setattr(base_app, 'do_passthrough', types.MethodType(do_passthrough, base_app)) - with pytest.raises(OSError) as excinfo: + with pytest.raises(OSError, match=expected_err): base_app.onecmd_plus_hooks('passthrough') - assert 'Pass me up' in str(excinfo.value) -def test_output_redirection(base_app): +def test_output_redirection(base_app) -> None: fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(fd) try: # Verify that writing to a file works - run_cmd(base_app, 'help > {}'.format(filename)) + run_cmd(base_app, f'help > {filename}') with open(filename) as f: content = f.read() verify_help_text(base_app, content) # Verify that appending to a file also works - run_cmd(base_app, 'help history >> {}'.format(filename)) + run_cmd(base_app, f'help history >> {filename}') with open(filename) as f: appended_content = f.read() assert appended_content.startswith(content) @@ -657,17 +649,17 @@ def test_output_redirection(base_app): os.remove(filename) -def test_output_redirection_to_nonexistent_directory(base_app): +def test_output_redirection_to_nonexistent_directory(base_app) -> None: filename = '~/fakedir/this_does_not_exist.txt' - out, err = run_cmd(base_app, 'help > {}'.format(filename)) + out, err = run_cmd(base_app, f'help > {filename}') assert 'Failed to redirect' in err[0] - out, err = run_cmd(base_app, 'help >> {}'.format(filename)) + out, err = run_cmd(base_app, f'help >> {filename}') assert 'Failed to redirect' in err[0] -def test_output_redirection_to_too_long_filename(base_app): +def test_output_redirection_to_too_long_filename(base_app) -> None: filename = ( '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' 'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh' @@ -676,21 +668,21 @@ def test_output_redirection_to_too_long_filename(base_app): 'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw' ) - out, err = run_cmd(base_app, 'help > {}'.format(filename)) + out, err = run_cmd(base_app, f'help > {filename}') assert 'Failed to redirect' in err[0] - out, err = run_cmd(base_app, 'help >> {}'.format(filename)) + out, err = run_cmd(base_app, f'help >> {filename}') assert 'Failed to redirect' in err[0] -def test_feedback_to_output_true(base_app): +def test_feedback_to_output_true(base_app) -> None: base_app.feedback_to_output = True base_app.timing = True f, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(f) try: - run_cmd(base_app, 'help > {}'.format(filename)) + run_cmd(base_app, f'help > {filename}') with open(filename) as f: content = f.readlines() assert content[-1].startswith('Elapsed: ') @@ -700,14 +692,14 @@ def test_feedback_to_output_true(base_app): os.remove(filename) -def test_feedback_to_output_false(base_app): +def test_feedback_to_output_false(base_app) -> None: base_app.feedback_to_output = False base_app.timing = True f, filename = tempfile.mkstemp(prefix='feedback_to_output', suffix='.txt') os.close(f) try: - out, err = run_cmd(base_app, 'help > {}'.format(filename)) + out, err = run_cmd(base_app, f'help > {filename}') with open(filename) as f: content = f.readlines() @@ -719,21 +711,21 @@ def test_feedback_to_output_false(base_app): os.remove(filename) -def test_disallow_redirection(base_app): +def test_disallow_redirection(base_app) -> None: # Set allow_redirection to False base_app.allow_redirection = False filename = 'test_allow_redirect.txt' # Verify output wasn't redirected - out, err = run_cmd(base_app, 'help > {}'.format(filename)) + out, err = run_cmd(base_app, f'help > {filename}') verify_help_text(base_app, out) # Verify that no file got created assert not os.path.exists(filename) -def test_pipe_to_shell(base_app): +def test_pipe_to_shell(base_app) -> None: if sys.platform == "win32": # Windows command = 'help | sort' @@ -743,26 +735,28 @@ def test_pipe_to_shell(base_app): command = 'help help | wc' out, err = run_cmd(base_app, command) - assert out and not err + assert out + assert not err -def test_pipe_to_shell_and_redirect(base_app): +def test_pipe_to_shell_and_redirect(base_app) -> None: filename = 'out.txt' if sys.platform == "win32": # Windows - command = 'help | sort > {}'.format(filename) + command = f'help | sort > {filename}' else: # Mac and Linux # Get help on help and pipe it's output to the input of the word count shell command - command = 'help help | wc > {}'.format(filename) + command = f'help help | wc > {filename}' out, err = run_cmd(base_app, command) - assert not out and not err + assert not out + assert not err assert os.path.exists(filename) os.remove(filename) -def test_pipe_to_shell_error(base_app): +def test_pipe_to_shell_error(base_app) -> None: # 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 @@ -777,14 +771,14 @@ def test_pipe_to_shell_error(base_app): # ValueError for headless Linux systems without Gtk installed # AssertionError can be raised by paste_klipper(). # PyperclipException for pyperclip-specific exceptions -except Exception: +except Exception: # noqa: BLE001 can_paste = False else: can_paste = True @pytest.mark.skipif(not can_paste, reason="Pyperclip could not find a copy/paste mechanism for your system") -def test_send_to_paste_buffer(base_app): +def test_send_to_paste_buffer(base_app) -> None: # Test writing to the PasteBuffer/Clipboard run_cmd(base_app, 'help >') paste_contents = cmd2.cmd2.get_paste_buffer() @@ -797,7 +791,7 @@ def test_send_to_paste_buffer(base_app): assert len(appended_contents) > len(paste_contents) -def test_get_paste_buffer_exception(base_app, mocker, capsys): +def test_get_paste_buffer_exception(base_app, mocker, capsys) -> None: # Force get_paste_buffer to throw an exception pastemock = mocker.patch('pyperclip.paste') pastemock.side_effect = ValueError('foo') @@ -809,10 +803,11 @@ def test_get_paste_buffer_exception(base_app, mocker, capsys): out, err = capsys.readouterr() assert out == '' # this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.paste - assert 'ValueError' in err and 'foo' in err + assert 'ValueError' in err + assert 'foo' in err -def test_allow_clipboard_initializer(base_app): +def test_allow_clipboard_initializer(base_app) -> None: assert base_app.allow_clipboard is True noclipcmd = cmd2.Cmd(allow_clipboard=False) assert noclipcmd.allow_clipboard is False @@ -822,14 +817,14 @@ def test_allow_clipboard_initializer(base_app): # before it tries to do anything with pyperclip, that's why we can # safely run this test without skipping it if pyperclip doesn't # work in the test environment, like we do for test_send_to_paste_buffer() -def test_allow_clipboard(base_app): +def test_allow_clipboard(base_app) -> None: base_app.allow_clipboard = False out, err = run_cmd(base_app, 'help >') assert not out assert "Clipboard access not allowed" in err -def test_base_timing(base_app): +def test_base_timing(base_app) -> None: base_app.feedback_to_output = False out, err = run_cmd(base_app, 'set timing True') expected = normalize( @@ -851,17 +846,15 @@ def _expected_no_editor_error(): if hasattr(sys, "pypy_translation_info"): expected_exception = 'EnvironmentError' - expected_text = normalize( - """ -EXCEPTION of type '{}' occurred with message: Please use 'set editor' to specify your text editing program of choice. + return normalize( + f""" +EXCEPTION of type '{expected_exception}' 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) +""" ) - return expected_text - -def test_base_debug(base_app): +def test_base_debug(base_app) -> None: # Purposely set the editor to None base_app.editor = None @@ -886,7 +879,7 @@ def test_base_debug(base_app): assert err[0].startswith('Traceback (most recent call last):') -def test_debug_not_settable(base_app): +def test_debug_not_settable(base_app) -> None: # Set debug to False and make it unsettable base_app.debug = False base_app.remove_settable('debug') @@ -898,12 +891,12 @@ def test_debug_not_settable(base_app): assert err == ['Invalid syntax: No closing quotation'] -def test_remove_settable_keyerror(base_app): +def test_remove_settable_keyerror(base_app) -> None: with pytest.raises(KeyError): base_app.remove_settable('fake') -def test_edit_file(base_app, request, monkeypatch): +def test_edit_file(base_app, request, monkeypatch) -> None: # 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' @@ -914,14 +907,14 @@ def test_edit_file(base_app, request, monkeypatch): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') - run_cmd(base_app, 'edit {}'.format(filename)) + run_cmd(base_app, f'edit {filename}') # 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): +def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch) -> None: """Test editor and file names with various patterns""" # Mock out the do_shell call to see what args are passed to it shell_mock = mock.MagicMock(name='do_shell') @@ -929,11 +922,11 @@ def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): base_app.editor = 'fooedit' file_name = utils.quote_string('nothingweird.py') - run_cmd(base_app, "edit {}".format(utils.quote_string(file_name))) - shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name))) + run_cmd(base_app, f"edit {utils.quote_string(file_name)}") + shell_mock.assert_called_once_with(f'"fooedit" {utils.quote_string(file_name)}') -def test_edit_file_with_spaces(base_app, request, monkeypatch): +def test_edit_file_with_spaces(base_app, request, monkeypatch) -> None: # 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' @@ -944,13 +937,13 @@ def test_edit_file_with_spaces(base_app, request, monkeypatch): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'my commands.txt') - run_cmd(base_app, 'edit "{}"'.format(filename)) + run_cmd(base_app, f'edit "{filename}"') # We think we have an editor, so should expect a Popen call m.assert_called_once() -def test_edit_blank(base_app, monkeypatch): +def test_edit_blank(base_app, monkeypatch) -> None: # 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' @@ -964,7 +957,7 @@ def test_edit_blank(base_app, monkeypatch): m.assert_called_once() -def test_base_py_interactive(base_app): +def test_base_py_interactive(base_app) -> None: # Mock out the InteractiveConsole.interact() call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='interact') InteractiveConsole.interact = m @@ -975,7 +968,7 @@ def test_base_py_interactive(base_app): m.assert_called_once() -def test_base_cmdloop_with_startup_commands(): +def test_base_cmdloop_with_startup_commands() -> None: intro = 'Hello World, this is an intro ...' # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args @@ -983,7 +976,7 @@ def test_base_cmdloop_with_startup_commands(): expected = intro + '\n' with mock.patch.object(sys, 'argv', testargs): - app = CreateOutsimApp() + app = create_outsim_app() app.use_rawinput = True @@ -994,11 +987,11 @@ def test_base_cmdloop_with_startup_commands(): assert out == expected -def test_base_cmdloop_without_startup_commands(): +def test_base_cmdloop_without_startup_commands() -> None: # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] with mock.patch.object(sys, 'argv', testargs): - app = CreateOutsimApp() + app = create_outsim_app() app.use_rawinput = True app.intro = 'Hello World, this is an intro ...' @@ -1015,11 +1008,11 @@ def test_base_cmdloop_without_startup_commands(): assert out == expected -def test_cmdloop_without_rawinput(): +def test_cmdloop_without_rawinput() -> None: # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] with mock.patch.object(sys, 'argv', testargs): - app = CreateOutsimApp() + app = create_outsim_app() app.use_rawinput = False app.echo = False @@ -1031,14 +1024,14 @@ def test_cmdloop_without_rawinput(): expected = app.intro + '\n' - with pytest.raises(OSError): + with pytest.raises(OSError): # noqa: PT011 app.cmdloop() out = app.stdout.getvalue() assert out == expected @pytest.mark.skipif(sys.platform.startswith('win'), reason="stty sane only run on Linux/Mac") -def test_stty_sane(base_app, monkeypatch): +def test_stty_sane(base_app, monkeypatch) -> None: """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)): # Mock out the subprocess.Popen call so we don't actually run stty sane @@ -1049,7 +1042,7 @@ def test_stty_sane(base_app, monkeypatch): m.assert_called_once_with(['stty', 'sane']) -def test_sigint_handler(base_app): +def test_sigint_handler(base_app) -> None: # No KeyboardInterrupt should be raised when using sigint_protection with base_app.sigint_protection: base_app.sigint_handler(signal.SIGINT, 1) @@ -1059,14 +1052,14 @@ def test_sigint_handler(base_app): base_app.sigint_handler(signal.SIGINT, 1) -def test_raise_keyboard_interrupt(base_app): +def test_raise_keyboard_interrupt(base_app) -> None: with pytest.raises(KeyboardInterrupt) as excinfo: base_app._raise_keyboard_interrupt() assert 'Got a keyboard interrupt' in str(excinfo.value) @pytest.mark.skipif(sys.platform.startswith('win'), reason="SIGTERM only handled on Linux/Mac") -def test_termination_signal_handler(base_app): +def test_termination_signal_handler(base_app) -> None: with pytest.raises(SystemExit) as excinfo: base_app.termination_signal_handler(signal.SIGHUP, 1) assert excinfo.value.code == signal.SIGHUP + 128 @@ -1077,7 +1070,7 @@ def test_termination_signal_handler(base_app): class HookFailureApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # register a postparsing hook method self.register_postparsing_hook(self.postparsing_precmd) @@ -1090,25 +1083,24 @@ def postparsing_precmd(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.P @pytest.fixture def hook_failure(): - app = HookFailureApp() - return app + return HookFailureApp() -def test_precmd_hook_success(base_app): +def test_precmd_hook_success(base_app) -> None: out = base_app.onecmd_plus_hooks('help') assert out is False -def test_precmd_hook_failure(hook_failure): +def test_precmd_hook_failure(hook_failure) -> None: out = hook_failure.onecmd_plus_hooks('help') assert out is True class SayApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_say(self, arg): + def do_say(self, arg) -> None: self.poutput(arg) @@ -1119,7 +1111,7 @@ def say_app(): return app -def test_ctrl_c_at_prompt(say_app): +def test_ctrl_c_at_prompt(say_app) -> None: # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] @@ -1133,12 +1125,12 @@ def test_ctrl_c_at_prompt(say_app): class ShellApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.default_to_shell = True -def test_default_to_shell(base_app, monkeypatch): +def test_default_to_shell(base_app, monkeypatch) -> None: if sys.platform.startswith('win'): line = 'dir' else: @@ -1152,7 +1144,7 @@ def test_default_to_shell(base_app, monkeypatch): assert m.called -def test_escaping_prompt(): +def test_escaping_prompt() -> None: from cmd2.rl_utils import ( rl_escape_prompt, rl_unescape_prompt, @@ -1183,80 +1175,74 @@ def test_escaping_prompt(): class HelpApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_squat(self, arg): + def do_squat(self, arg) -> None: """This docstring help will never be shown because the help_squat method overrides it.""" - pass - def help_squat(self): + def help_squat(self) -> None: self.stdout.write('This command does diddly squat...\n') - def do_edit(self, arg): + def do_edit(self, arg) -> None: """This overrides the edit command and does nothing.""" - pass # This command will be in the "undocumented" section of the help menu - def do_undoc(self, arg): + def do_undoc(self, arg) -> None: pass - def do_multiline_docstr(self, arg): - """ - This documentation + def do_multiline_docstr(self, arg) -> None: + """This documentation is multiple lines and there are no tabs """ - pass parser_cmd_parser = cmd2.Cmd2ArgumentParser(description="This is the description.") @cmd2.with_argparser(parser_cmd_parser) - def do_parser_cmd(self, args): + def do_parser_cmd(self, args) -> None: """This is the docstring.""" - pass @pytest.fixture def help_app(): - app = HelpApp() - return app + return HelpApp() -def test_custom_command_help(help_app): +def test_custom_command_help(help_app) -> None: out, err = run_cmd(help_app, 'help squat') expected = normalize('This command does diddly squat...') assert out == expected assert help_app.last_result is True -def test_custom_help_menu(help_app): +def test_custom_help_menu(help_app) -> None: out, err = run_cmd(help_app, 'help') verify_help_text(help_app, out) -def test_help_undocumented(help_app): +def test_help_undocumented(help_app) -> None: out, err = run_cmd(help_app, 'help undoc') assert err[0].startswith("No help on undoc") assert help_app.last_result is False -def test_help_overridden_method(help_app): +def test_help_overridden_method(help_app) -> None: out, err = run_cmd(help_app, 'help edit') expected = normalize('This overrides the edit command and does nothing.') assert out == expected assert help_app.last_result is True -def test_help_multiline_docstring(help_app): +def test_help_multiline_docstring(help_app) -> None: out, err = run_cmd(help_app, 'help multiline_docstr') expected = normalize('This documentation\nis multiple lines\nand there are no\ntabs') assert out == expected assert help_app.last_result is True -def test_help_verbose_uses_parser_description(help_app: HelpApp): +def test_help_verbose_uses_parser_description(help_app: HelpApp) -> None: out, err = run_cmd(help_app, 'help --verbose') verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description]) @@ -1264,57 +1250,53 @@ def test_help_verbose_uses_parser_description(help_app: HelpApp): class HelpCategoriesApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @cmd2.with_category('Some Category') - def do_diddly(self, arg): + def do_diddly(self, arg) -> None: """This command does diddly""" - pass # This command will be in the "Some Category" section of the help menu even though it has no docstring @cmd2.with_category("Some Category") - def do_cat_nodoc(self, arg): + def do_cat_nodoc(self, arg) -> None: pass - def do_squat(self, arg): + def do_squat(self, arg) -> None: """This docstring help will never be shown because the help_squat method overrides it.""" - pass - def help_squat(self): + def help_squat(self) -> None: self.stdout.write('This command does diddly squat...\n') - def do_edit(self, arg): + def do_edit(self, arg) -> None: """This overrides the edit command and does nothing.""" - pass cmd2.categorize((do_squat, do_edit), 'Custom Category') # This command will be in the "undocumented" section of the help menu - def do_undoc(self, arg): + def do_undoc(self, arg) -> None: pass @pytest.fixture def helpcat_app(): - app = HelpCategoriesApp() - return app + return HelpCategoriesApp() -def test_help_cat_base(helpcat_app): +def test_help_cat_base(helpcat_app) -> None: out, err = run_cmd(helpcat_app, 'help') assert helpcat_app.last_result is True verify_help_text(helpcat_app, out) -def test_help_cat_verbose(helpcat_app): +def test_help_cat_verbose(helpcat_app) -> None: out, err = run_cmd(helpcat_app, 'help --verbose') assert helpcat_app.last_result is True verify_help_text(helpcat_app, out) class SelectApp(cmd2.Cmd): - def do_eat(self, arg): + def do_eat(self, arg) -> None: """Eat something, with a selection of sauces to choose from.""" # Pass in a single string of space-separated selections sauce = self.select('sweet salty', 'Sauce? ') @@ -1322,30 +1304,30 @@ def do_eat(self, arg): result = result.format(food=arg, sauce=sauce) self.stdout.write(result + '\n') - def do_study(self, arg): + def do_study(self, arg) -> None: """Learn something, with a selection of subjects to choose from.""" # Pass in a list of strings for selections subject = self.select(['math', 'science'], 'Subject? ') - result = 'Good luck learning {}!\n'.format(subject) + result = f'Good luck learning {subject}!\n' self.stdout.write(result) - def do_procrastinate(self, arg): + def do_procrastinate(self, arg) -> None: """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? ' ) - result = 'Have fun procrasinating with {}!\n'.format(leisure_activity) + result = f'Have fun procrasinating with {leisure_activity}!\n' self.stdout.write(result) - def do_play(self, arg): + def do_play(self, arg) -> None: """Play your favorite musical instrument.""" # Pass in an uneven list of tuples for selections instrument = self.select([('Guitar', 'Electric Guitar'), ('Drums',)], 'Instrument? ') - result = 'Charm us with the {}...\n'.format(instrument) + result = f'Charm us with the {instrument}...\n' self.stdout.write(result) - def do_return_type(self, arg): + def do_return_type(self, arg) -> None: """Test that return values can be non-strings""" choice = self.select([(1, 'Integer'), ("test_str", 'String'), (self.do_play, 'Method')], 'Choice? ') result = f'The return type is {type(choice)}\n' @@ -1354,23 +1336,22 @@ def do_return_type(self, arg): @pytest.fixture def select_app(): - app = SelectApp() - return app + return SelectApp() -def test_select_options(select_app, monkeypatch): +def test_select_options(select_app, monkeypatch) -> None: # 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') monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) food = 'bacon' - out, err = run_cmd(select_app, "eat {}".format(food)) + out, err = run_cmd(select_app, f"eat {food}") expected = normalize( - """ + f""" 1. sweet 2. salty -{} with salty sauce, yum! -""".format(food) +{food} with salty sauce, yum! +""" ) # Make sure our mock was called with the expected arguments @@ -1380,7 +1361,7 @@ def test_select_options(select_app, monkeypatch): assert out == expected -def test_select_invalid_option_too_big(select_app, monkeypatch): +def test_select_invalid_option_too_big(select_app, monkeypatch) -> None: # 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') @@ -1389,14 +1370,14 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) food = 'fish' - out, err = run_cmd(select_app, "eat {}".format(food)) + out, err = run_cmd(select_app, f"eat {food}") expected = normalize( - """ + f""" 1. sweet 2. salty '3' isn't a valid choice. Pick a number between 1 and 2: -{} with sweet sauce, yum! -""".format(food) +{food} with sweet sauce, yum! +""" ) # Make sure our mock was called exactly twice with the expected arguments @@ -1409,7 +1390,7 @@ def test_select_invalid_option_too_big(select_app, monkeypatch): assert out == expected -def test_select_invalid_option_too_small(select_app, monkeypatch): +def test_select_invalid_option_too_small(select_app, monkeypatch) -> None: # 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') @@ -1418,14 +1399,14 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) food = 'fish' - out, err = run_cmd(select_app, "eat {}".format(food)) + out, err = run_cmd(select_app, f"eat {food}") expected = normalize( - """ + f""" 1. sweet 2. salty '0' isn't a valid choice. Pick a number between 1 and 2: -{} with sweet sauce, yum! -""".format(food) +{food} with sweet sauce, yum! +""" ) # Make sure our mock was called exactly twice with the expected arguments @@ -1438,7 +1419,7 @@ def test_select_invalid_option_too_small(select_app, monkeypatch): assert out == expected -def test_select_list_of_strings(select_app, monkeypatch): +def test_select_list_of_strings(select_app, monkeypatch) -> None: # 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) @@ -1459,7 +1440,7 @@ def test_select_list_of_strings(select_app, monkeypatch): assert out == expected -def test_select_list_of_tuples(select_app, monkeypatch): +def test_select_list_of_tuples(select_app, monkeypatch) -> None: # 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) @@ -1480,7 +1461,7 @@ def test_select_list_of_tuples(select_app, monkeypatch): assert out == expected -def test_select_uneven_list_of_tuples(select_app, monkeypatch): +def test_select_uneven_list_of_tuples(select_app, monkeypatch) -> None: # 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) @@ -1502,26 +1483,26 @@ def test_select_uneven_list_of_tuples(select_app, monkeypatch): @pytest.mark.parametrize( - 'selection, type_str', + ('selection', 'type_str'), [ ('1', ""), ('2', ""), ('3', ""), ], ) -def test_select_return_type(select_app, monkeypatch, selection, type_str): +def test_select_return_type(select_app, monkeypatch, selection, type_str) -> None: # 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=selection) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) out, err = run_cmd(select_app, "return_type") expected = normalize( - """ + f""" 1. Integer 2. String 3. Method -The return type is {} -""".format(type_str) +The return type is {type_str} +""" ) # Make sure our mock was called with the expected arguments @@ -1531,13 +1512,13 @@ def test_select_return_type(select_app, monkeypatch, selection, type_str): assert out == expected -def test_select_eof(select_app, monkeypatch): +def test_select_eof(select_app, monkeypatch) -> None: # Ctrl-D during select causes an EOFError that just reprompts the user read_input_mock = mock.MagicMock(name='read_input', side_effect=[EOFError, 2]) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) food = 'fish' - out, err = run_cmd(select_app, "eat {}".format(food)) + out, err = run_cmd(select_app, f"eat {food}") # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' @@ -1546,7 +1527,7 @@ def test_select_eof(select_app, monkeypatch): assert read_input_mock.call_count == 2 -def test_select_ctrl_c(outsim_app, monkeypatch): +def test_select_ctrl_c(outsim_app, monkeypatch) -> None: # Ctrl-C during select prints ^C and raises a KeyboardInterrupt read_input_mock = mock.MagicMock(name='read_input', side_effect=KeyboardInterrupt) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) @@ -1563,14 +1544,14 @@ class HelpNoDocstringApp(cmd2.Cmd): 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): + def do_greet(self, opts, arg) -> None: arg = ''.join(arg) if opts.shout: arg = arg.upper() self.stdout.write(arg + '\n') -def test_help_with_no_docstring(capsys): +def test_help_with_no_docstring(capsys) -> None: app = HelpNoDocstringApp() app.onecmd_plus_hooks('greet -h') out, err = capsys.readouterr() @@ -1588,14 +1569,14 @@ def test_help_with_no_docstring(capsys): class MultilineApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, multiline_commands=['orate'], **kwargs) orate_parser = cmd2.Cmd2ArgumentParser() orate_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE") @cmd2.with_argparser(orate_parser, with_unknown_args=True) - def do_orate(self, opts, arg): + def do_orate(self, opts, arg) -> None: arg = ''.join(arg) if opts.shout: arg = arg.upper() @@ -1604,16 +1585,15 @@ def do_orate(self, opts, arg): @pytest.fixture def multiline_app(): - app = MultilineApp() - return app + return MultilineApp() -def test_multiline_complete_empty_statement_raises_exception(multiline_app): +def test_multiline_complete_empty_statement_raises_exception(multiline_app) -> None: with pytest.raises(exceptions.EmptyStatement): multiline_app._complete_statement('') -def test_multiline_complete_statement_without_terminator(multiline_app): +def test_multiline_complete_statement_without_terminator(multiline_app) -> None: # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input m = mock.MagicMock(name='input', return_value='\n') @@ -1621,14 +1601,14 @@ def test_multiline_complete_statement_without_terminator(multiline_app): command = 'orate' args = 'hello world' - line = '{} {}'.format(command, args) + line = f'{command} {args}' statement = multiline_app._complete_statement(line) assert statement == args assert statement.command == command assert statement.multiline_command == command -def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): +def test_multiline_complete_statement_with_unclosed_quotes(multiline_app) -> None: # Mock out the input call so we don't actually wait for a user's response # on stdin when it looks for more input m = mock.MagicMock(name='input', side_effect=['quotes', '" now closed;']) @@ -1642,7 +1622,7 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): assert statement.terminator == ';' -def test_multiline_input_line_to_statement(multiline_app): +def test_multiline_input_line_to_statement(multiline_app) -> None: # Verify _input_line_to_statement saves the fully entered input line for multiline commands # Mock out the input call so we don't actually wait for a user's response @@ -1658,7 +1638,7 @@ def test_multiline_input_line_to_statement(multiline_app): assert statement.multiline_command == 'orate' -def test_multiline_history_no_prior_history(multiline_app): +def test_multiline_history_no_prior_history(multiline_app) -> None: # Test no existing history prior to typing the command m = mock.MagicMock(name='input', side_effect=['person', '\n']) builtins.input = m @@ -1675,7 +1655,7 @@ def test_multiline_history_no_prior_history(multiline_app): assert readline.get_history_item(1) == "orate hi person" -def test_multiline_history_first_line_matches_prev_entry(multiline_app): +def test_multiline_history_first_line_matches_prev_entry(multiline_app) -> None: # Test when first line of multiline command matches previous history entry m = mock.MagicMock(name='input', side_effect=['person', '\n']) builtins.input = m @@ -1694,7 +1674,7 @@ def test_multiline_history_first_line_matches_prev_entry(multiline_app): assert readline.get_history_item(2) == "orate hi person" -def test_multiline_history_matches_prev_entry(multiline_app): +def test_multiline_history_matches_prev_entry(multiline_app) -> None: # Test combined multiline command that matches previous history entry m = mock.MagicMock(name='input', side_effect=['person', '\n']) builtins.input = m @@ -1712,7 +1692,7 @@ def test_multiline_history_matches_prev_entry(multiline_app): assert readline.get_history_item(1) == "orate hi person" -def test_multiline_history_does_not_match_prev_entry(multiline_app): +def test_multiline_history_does_not_match_prev_entry(multiline_app) -> None: # Test combined multiline command that does not match previous history entry m = mock.MagicMock(name='input', side_effect=['person', '\n']) builtins.input = m @@ -1731,7 +1711,7 @@ def test_multiline_history_does_not_match_prev_entry(multiline_app): assert readline.get_history_item(2) == "orate hi person" -def test_multiline_history_with_quotes(multiline_app): +def test_multiline_history_with_quotes(multiline_app) -> None: # Test combined multiline command with quotes m = mock.MagicMock(name='input', side_effect=[' and spaces ', ' "', ' in', 'quotes.', ';']) builtins.input = m @@ -1753,73 +1733,73 @@ def test_multiline_history_with_quotes(multiline_app): class CommandResultApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_affirmative(self, arg): + def do_affirmative(self, arg) -> None: self.last_result = cmd2.CommandResult(arg, data=True) - def do_negative(self, arg): + def do_negative(self, arg) -> None: self.last_result = cmd2.CommandResult(arg, data=False) - def do_affirmative_no_data(self, arg): + def do_affirmative_no_data(self, arg) -> None: self.last_result = cmd2.CommandResult(arg) - def do_negative_no_data(self, arg): + def do_negative_no_data(self, arg) -> None: self.last_result = cmd2.CommandResult('', arg) @pytest.fixture def commandresult_app(): - app = CommandResultApp() - return app + return CommandResultApp() -def test_commandresult_truthy(commandresult_app): +def test_commandresult_truthy(commandresult_app) -> None: arg = 'foo' - run_cmd(commandresult_app, 'affirmative {}'.format(arg)) + run_cmd(commandresult_app, f'affirmative {arg}') assert commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult(arg, data=True) - run_cmd(commandresult_app, 'affirmative_no_data {}'.format(arg)) + run_cmd(commandresult_app, f'affirmative_no_data {arg}') assert commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult(arg) -def test_commandresult_falsy(commandresult_app): +def test_commandresult_falsy(commandresult_app) -> None: arg = 'bar' - run_cmd(commandresult_app, 'negative {}'.format(arg)) + run_cmd(commandresult_app, f'negative {arg}') assert not commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult(arg, data=False) - run_cmd(commandresult_app, 'negative_no_data {}'.format(arg)) + run_cmd(commandresult_app, f'negative_no_data {arg}') assert not commandresult_app.last_result assert commandresult_app.last_result == cmd2.CommandResult('', arg) -def test_is_text_file_bad_input(base_app): +@pytest.mark.skipif(sys.platform.startswith('win'), reason="Test is problematic on GitHub Actions Windows runners") +def test_is_text_file_bad_input(base_app) -> None: # Test with a non-existent file - with pytest.raises(OSError): + with pytest.raises(FileNotFoundError): utils.is_text_file('does_not_exist.txt') # Test with a directory - with pytest.raises(OSError): + with pytest.raises(IsADirectoryError): utils.is_text_file('.') -def test_eof(base_app): +def test_eof(base_app) -> None: # Only thing to verify is that it returns True assert base_app.do_eof('') assert base_app.last_result is True -def test_quit(base_app): +def test_quit(base_app) -> None: # Only thing to verify is that it returns True assert base_app.do_quit('') assert base_app.last_result is True -def test_echo(capsys): +def test_echo(capsys) -> None: app = cmd2.Cmd() app.echo = True commands = ['help history'] @@ -1827,10 +1807,10 @@ def test_echo(capsys): app.runcmds_plus_hooks(commands) out, err = capsys.readouterr() - assert out.startswith('{}{}\n'.format(app.prompt, commands[0]) + HELP_HISTORY.split()[0]) + assert out.startswith(f'{app.prompt}{commands[0]}\n' + HELP_HISTORY.split()[0]) -def test_read_input_rawinput_true(capsys, monkeypatch): +def test_read_input_rawinput_true(capsys, monkeypatch) -> None: prompt_str = 'the_prompt' input_str = 'some input' @@ -1885,7 +1865,7 @@ def test_read_input_rawinput_true(capsys, monkeypatch): line = app.read_input(prompt_str) out, err = capsys.readouterr() assert line == input_str - assert out == "{}{}\n".format(prompt_str, input_str) + assert out == f"{prompt_str}{input_str}\n" # echo False app.echo = False @@ -1895,7 +1875,7 @@ def test_read_input_rawinput_true(capsys, monkeypatch): assert not out -def test_read_input_rawinput_false(capsys, monkeypatch): +def test_read_input_rawinput_false(capsys, monkeypatch) -> None: prompt_str = 'the_prompt' input_str = 'some input' @@ -1903,7 +1883,7 @@ def make_app(isatty: bool, empty_input: bool = False): """Make a cmd2 app with a custom stdin""" app_input_str = '' if empty_input else input_str - fakein = io.StringIO('{}'.format(app_input_str)) + fakein = io.StringIO(f'{app_input_str}') fakein.isatty = mock.MagicMock(name='isatty', return_value=isatty) new_app = cmd2.Cmd(stdin=fakein) @@ -1930,7 +1910,7 @@ def make_app(isatty: bool, empty_input: bool = False): line = app.read_input(prompt_str) out, err = capsys.readouterr() assert line == input_str - assert out == "{}{}\n".format(prompt_str, input_str) + assert out == f"{prompt_str}{input_str}\n" # isatty is False, echo is False app = make_app(isatty=False) @@ -1948,7 +1928,7 @@ def make_app(isatty: bool, empty_input: bool = False): assert not out -def test_read_command_line_eof(base_app, monkeypatch): +def test_read_command_line_eof(base_app, monkeypatch) -> None: read_input_mock = mock.MagicMock(name='read_input', side_effect=EOFError) monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock) @@ -1956,7 +1936,7 @@ def test_read_command_line_eof(base_app, monkeypatch): assert line == 'eof' -def test_poutput_string(outsim_app): +def test_poutput_string(outsim_app) -> None: msg = 'This is a test' outsim_app.poutput(msg) out = outsim_app.stdout.getvalue() @@ -1964,7 +1944,7 @@ def test_poutput_string(outsim_app): assert out == expected -def test_poutput_zero(outsim_app): +def test_poutput_zero(outsim_app) -> None: msg = 0 outsim_app.poutput(msg) out = outsim_app.stdout.getvalue() @@ -1972,7 +1952,7 @@ def test_poutput_zero(outsim_app): assert out == expected -def test_poutput_empty_string(outsim_app): +def test_poutput_empty_string(outsim_app) -> None: msg = '' outsim_app.poutput(msg) out = outsim_app.stdout.getvalue() @@ -1980,7 +1960,7 @@ def test_poutput_empty_string(outsim_app): assert out == expected -def test_poutput_none(outsim_app): +def test_poutput_none(outsim_app) -> None: msg = None outsim_app.poutput(msg) out = outsim_app.stdout.getvalue() @@ -1989,7 +1969,7 @@ def test_poutput_none(outsim_app): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_poutput_ansi_always(outsim_app): +def test_poutput_ansi_always(outsim_app) -> None: msg = 'Hello World' colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN) outsim_app.poutput(colored_msg) @@ -2000,7 +1980,7 @@ def test_poutput_ansi_always(outsim_app): @with_ansi_style(ansi.AllowStyle.NEVER) -def test_poutput_ansi_never(outsim_app): +def test_poutput_ansi_never(outsim_app) -> None: msg = 'Hello World' colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN) outsim_app.poutput(colored_msg) @@ -2024,7 +2004,7 @@ def test_poutput_ansi_never(outsim_app): ] -def test_get_alias_completion_items(base_app): +def test_get_alias_completion_items(base_app) -> None: run_cmd(base_app, 'alias create fake run_pyscript') run_cmd(base_app, 'alias create ls !ls -hal') @@ -2037,7 +2017,7 @@ def test_get_alias_completion_items(base_app): assert cur_res.description.rstrip() == base_app.aliases[cur_res] -def test_get_macro_completion_items(base_app): +def test_get_macro_completion_items(base_app) -> None: run_cmd(base_app, 'macro create foo !echo foo') run_cmd(base_app, 'macro create bar !echo bar') @@ -2050,7 +2030,7 @@ def test_get_macro_completion_items(base_app): assert cur_res.description.rstrip() == base_app.macros[cur_res].value -def test_get_settable_completion_items(base_app): +def test_get_settable_completion_items(base_app) -> None: results = base_app._get_settable_completion_items() assert len(results) == len(base_app.settables) @@ -2068,13 +2048,13 @@ def test_get_settable_completion_items(base_app): assert cur_settable.description[0:10] in cur_res.description -def test_alias_no_subcommand(base_app): +def test_alias_no_subcommand(base_app) -> None: 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): +def test_alias_create(base_app) -> None: # Create the alias out, err = run_cmd(base_app, 'alias create fake run_pyscript') assert out == normalize("Alias 'fake' created") @@ -2108,7 +2088,7 @@ def test_alias_create(base_app): assert base_app.last_result['fake'] == "help" -def test_alias_create_with_quoted_tokens(base_app): +def test_alias_create_with_quoted_tokens(base_app) -> None: """Demonstrate that quotes in alias value will be preserved""" alias_name = "fake" alias_command = 'help ">" "out file.txt" ";"' @@ -2126,27 +2106,27 @@ def test_alias_create_with_quoted_tokens(base_app): @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)) +def test_alias_create_invalid_name(base_app, alias_name, capsys) -> None: + out, err = run_cmd(base_app, f'alias create {alias_name} help') assert "Invalid alias name" in err[0] assert base_app.last_result is False -def test_alias_create_with_command_name(base_app): +def test_alias_create_with_command_name(base_app) -> None: out, err = run_cmd(base_app, 'alias create help stuff') assert "Alias cannot have the same name as a command" in err[0] assert base_app.last_result is False -def test_alias_create_with_macro_name(base_app): +def test_alias_create_with_macro_name(base_app) -> None: macro = "my_macro" - run_cmd(base_app, 'macro create {} help'.format(macro)) - out, err = run_cmd(base_app, 'alias create {} help'.format(macro)) + run_cmd(base_app, f'macro create {macro} help') + out, err = run_cmd(base_app, f'alias create {macro} help') assert "Alias cannot have the same name as a macro" in err[0] assert base_app.last_result is False -def test_alias_that_resolves_into_comment(base_app): +def test_alias_that_resolves_into_comment(base_app) -> None: # Create the alias out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah') assert out == normalize("Alias 'fake' created") @@ -2157,14 +2137,14 @@ def test_alias_that_resolves_into_comment(base_app): assert not err -def test_alias_list_invalid_alias(base_app): +def test_alias_list_invalid_alias(base_app) -> None: # Look up invalid alias out, err = run_cmd(base_app, 'alias list invalid') assert "Alias 'invalid' not found" in err[0] assert base_app.last_result == {} -def test_alias_delete(base_app): +def test_alias_delete(base_app) -> None: # Create an alias run_cmd(base_app, 'alias create fake run_pyscript') @@ -2174,29 +2154,29 @@ def test_alias_delete(base_app): assert base_app.last_result is True -def test_alias_delete_all(base_app): +def test_alias_delete_all(base_app) -> None: out, err = run_cmd(base_app, 'alias delete --all') assert out == normalize("All aliases deleted") assert base_app.last_result is True -def test_alias_delete_non_existing(base_app): +def test_alias_delete_non_existing(base_app) -> None: out, err = run_cmd(base_app, 'alias delete fake') assert "Alias 'fake' does not exist" in err[0] assert base_app.last_result is True -def test_alias_delete_no_name(base_app): +def test_alias_delete_no_name(base_app) -> None: out, err = run_cmd(base_app, 'alias delete') assert "Either --all or alias name(s)" in err[0] assert base_app.last_result is False -def test_multiple_aliases(base_app): +def test_multiple_aliases(base_app) -> None: alias1 = 'h1' alias2 = 'h2' - run_cmd(base_app, 'alias create {} help'.format(alias1)) - run_cmd(base_app, 'alias create {} help -v'.format(alias2)) + run_cmd(base_app, f'alias create {alias1} help') + run_cmd(base_app, f'alias create {alias2} help -v') out, err = run_cmd(base_app, alias1) verify_help_text(base_app, out) @@ -2204,13 +2184,13 @@ def test_multiple_aliases(base_app): verify_help_text(base_app, out) -def test_macro_no_subcommand(base_app): +def test_macro_no_subcommand(base_app) -> None: 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): +def test_macro_create(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript') assert out == normalize("Macro 'fake' created") @@ -2244,7 +2224,7 @@ def test_macro_create(base_app): assert base_app.last_result['fake'] == "help" -def test_macro_create_with_quoted_tokens(base_app): +def test_macro_create_with_quoted_tokens(base_app) -> None: """Demonstrate that quotes in macro value will be preserved""" macro_name = "fake" macro_command = 'help ">" "out file.txt" ";"' @@ -2262,27 +2242,27 @@ def test_macro_create_with_quoted_tokens(base_app): @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)) +def test_macro_create_invalid_name(base_app, macro_name) -> None: + out, err = run_cmd(base_app, f'macro create {macro_name} help') assert "Invalid macro name" in err[0] assert base_app.last_result is False -def test_macro_create_with_command_name(base_app): +def test_macro_create_with_command_name(base_app) -> None: out, err = run_cmd(base_app, 'macro create help stuff') assert "Macro cannot have the same name as a command" in err[0] assert base_app.last_result is False -def test_macro_create_with_alias_name(base_app): +def test_macro_create_with_alias_name(base_app) -> None: macro = "my_macro" - run_cmd(base_app, 'alias create {} help'.format(macro)) - out, err = run_cmd(base_app, 'macro create {} help'.format(macro)) + run_cmd(base_app, f'alias create {macro} help') + out, err = run_cmd(base_app, f'macro create {macro} help') assert "Macro cannot have the same name as an alias" in err[0] assert base_app.last_result is False -def test_macro_create_with_args(base_app): +def test_macro_create_with_args(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} {2}') assert out == normalize("Macro 'fake' created") @@ -2292,7 +2272,7 @@ def test_macro_create_with_args(base_app): verify_help_text(base_app, out) -def test_macro_create_with_escaped_args(base_app): +def test_macro_create_with_escaped_args(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake help {{1}}') assert out == normalize("Macro 'fake' created") @@ -2302,7 +2282,7 @@ def test_macro_create_with_escaped_args(base_app): assert err[0].startswith('No help on {1}') -def test_macro_usage_with_missing_args(base_app): +def test_macro_usage_with_missing_args(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1} {2}') assert out == normalize("Macro 'fake' created") @@ -2312,7 +2292,7 @@ def test_macro_usage_with_missing_args(base_app): assert "expects at least 2 arguments" in err[0] -def test_macro_usage_with_exta_args(base_app): +def test_macro_usage_with_exta_args(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake help {1}') assert out == normalize("Macro 'fake' created") @@ -2322,21 +2302,21 @@ def test_macro_usage_with_exta_args(base_app): assert "Usage: alias create" in out[0] -def test_macro_create_with_missing_arg_nums(base_app): +def test_macro_create_with_missing_arg_nums(base_app) -> None: # 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] assert base_app.last_result is False -def test_macro_create_with_invalid_arg_num(base_app): +def test_macro_create_with_invalid_arg_num(base_app) -> None: # 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] assert base_app.last_result is False -def test_macro_create_with_unicode_numbered_arg(base_app): +def test_macro_create_with_unicode_numbered_arg(base_app) -> None: # Create the macro expecting 1 argument out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}') assert out == normalize("Macro 'fake' created") @@ -2346,13 +2326,13 @@ def test_macro_create_with_unicode_numbered_arg(base_app): assert "expects at least 1 argument" in err[0] -def test_macro_create_with_missing_unicode_arg_nums(base_app): +def test_macro_create_with_missing_unicode_arg_nums(base_app) -> None: 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] assert base_app.last_result is False -def test_macro_that_resolves_into_comment(base_app): +def test_macro_that_resolves_into_comment(base_app) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} blah blah') assert out == normalize("Macro 'fake' created") @@ -2363,14 +2343,14 @@ def test_macro_that_resolves_into_comment(base_app): assert not err -def test_macro_list_invalid_macro(base_app): +def test_macro_list_invalid_macro(base_app) -> None: # Look up invalid macro out, err = run_cmd(base_app, 'macro list invalid') assert "Macro 'invalid' not found" in err[0] assert base_app.last_result == {} -def test_macro_delete(base_app): +def test_macro_delete(base_app) -> None: # Create an macro run_cmd(base_app, 'macro create fake run_pyscript') @@ -2380,29 +2360,29 @@ def test_macro_delete(base_app): assert base_app.last_result is True -def test_macro_delete_all(base_app): +def test_macro_delete_all(base_app) -> None: out, err = run_cmd(base_app, 'macro delete --all') assert out == normalize("All macros deleted") assert base_app.last_result is True -def test_macro_delete_non_existing(base_app): +def test_macro_delete_non_existing(base_app) -> None: out, err = run_cmd(base_app, 'macro delete fake') assert "Macro 'fake' does not exist" in err[0] assert base_app.last_result is True -def test_macro_delete_no_name(base_app): +def test_macro_delete_no_name(base_app) -> None: out, err = run_cmd(base_app, 'macro delete') assert "Either --all or macro name(s)" in err[0] assert base_app.last_result is False -def test_multiple_macros(base_app): +def test_multiple_macros(base_app) -> None: macro1 = 'h1' macro2 = 'h2' - run_cmd(base_app, 'macro create {} help'.format(macro1)) - run_cmd(base_app, 'macro create {} help -v'.format(macro2)) + run_cmd(base_app, f'macro create {macro1} help') + run_cmd(base_app, f'macro create {macro2} help -v') out, err = run_cmd(base_app, macro1) verify_help_text(base_app, out) @@ -2411,7 +2391,7 @@ def test_multiple_macros(base_app): assert len(out2) > len(out) -def test_nonexistent_macro(base_app): +def test_nonexistent_macro(base_app) -> None: from cmd2.parsing import ( StatementParser, ) @@ -2427,7 +2407,7 @@ def test_nonexistent_macro(base_app): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_perror_style(base_app, capsys): +def test_perror_style(base_app, capsys) -> None: msg = 'testing...' end = '\n' base_app.perror(msg) @@ -2436,7 +2416,7 @@ def test_perror_style(base_app, capsys): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_perror_no_style(base_app, capsys): +def test_perror_no_style(base_app, capsys) -> None: msg = 'testing...' end = '\n' base_app.perror(msg, apply_style=False) @@ -2445,7 +2425,7 @@ def test_perror_no_style(base_app, capsys): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_pexcept_style(base_app, capsys): +def test_pexcept_style(base_app, capsys) -> None: msg = Exception('testing...') base_app.pexcept(msg) @@ -2454,7 +2434,7 @@ def test_pexcept_style(base_app, capsys): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_pexcept_no_style(base_app, capsys): +def test_pexcept_no_style(base_app, capsys) -> None: msg = Exception('testing...') base_app.pexcept(msg, apply_style=False) @@ -2463,7 +2443,7 @@ def test_pexcept_no_style(base_app, capsys): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_pexcept_not_exception(base_app, capsys): +def test_pexcept_not_exception(base_app, capsys) -> None: # Pass in a msg that is not an Exception object msg = False @@ -2472,7 +2452,7 @@ def test_pexcept_not_exception(base_app, capsys): assert err.startswith(ansi.style_error(msg)) -def test_ppaged(outsim_app): +def test_ppaged(outsim_app) -> None: msg = 'testing...' end = '\n' outsim_app.ppaged(msg) @@ -2481,7 +2461,7 @@ def test_ppaged(outsim_app): @with_ansi_style(ansi.AllowStyle.TERMINAL) -def test_ppaged_strips_ansi_when_redirecting(outsim_app): +def test_ppaged_strips_ansi_when_redirecting(outsim_app) -> None: msg = 'testing...' end = '\n' outsim_app._redirecting = True @@ -2491,7 +2471,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): +def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app) -> None: msg = 'testing...' end = '\n' outsim_app._redirecting = True @@ -2505,7 +2485,7 @@ def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): # command parsing by parent methods we don't override # don't need to test all the parsing logic here, because # parseline just calls StatementParser.parse_command_only() -def test_parseline_empty(base_app): +def test_parseline_empty(base_app) -> None: statement = '' command, args, line = base_app.parseline(statement) assert not command @@ -2513,7 +2493,7 @@ def test_parseline_empty(base_app): assert not line -def test_parseline_quoted(base_app): +def test_parseline_quoted(base_app) -> None: statement = " command with 'partially completed quotes " command, args, line = base_app.parseline(statement) assert command == 'command' @@ -2521,7 +2501,7 @@ def test_parseline_quoted(base_app): assert line == statement.lstrip() -def test_onecmd_raw_str_continue(outsim_app): +def test_onecmd_raw_str_continue(outsim_app) -> None: line = "help" stop = outsim_app.onecmd(line) out = outsim_app.stdout.getvalue() @@ -2529,7 +2509,7 @@ def test_onecmd_raw_str_continue(outsim_app): verify_help_text(outsim_app, out) -def test_onecmd_raw_str_quit(outsim_app): +def test_onecmd_raw_str_quit(outsim_app) -> None: line = "quit" stop = outsim_app.onecmd(line) out = outsim_app.stdout.getvalue() @@ -2537,7 +2517,7 @@ def test_onecmd_raw_str_quit(outsim_app): assert out == '' -def test_onecmd_add_to_history(outsim_app): +def test_onecmd_add_to_history(outsim_app) -> None: line = "help" saved_hist_len = len(outsim_app.history) @@ -2554,7 +2534,7 @@ def test_onecmd_add_to_history(outsim_app): assert new_hist_len == saved_hist_len -def test_get_all_commands(base_app): +def test_get_all_commands(base_app) -> None: # Verify that the base app has the expected commands commands = base_app.get_all_commands() expected_commands = [ @@ -2577,22 +2557,22 @@ def test_get_all_commands(base_app): assert commands == expected_commands -def test_get_help_topics(base_app): +def test_get_help_topics(base_app) -> None: # 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(): +def test_get_help_topics_hidden() -> None: # Verify get_help_topics() filters out hidden commands class TestApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_my_cmd(self, args): + def do_my_cmd(self, args) -> None: pass - def help_my_cmd(self, args): + def help_my_cmd(self, args) -> None: pass app = TestApp() @@ -2605,7 +2585,7 @@ def help_my_cmd(self, args): class ReplWithExitCode(cmd2.Cmd): """Example cmd2 application where we can specify an exit code when existing.""" - def __init__(self): + def __init__(self) -> None: super().__init__(allow_cli_args=False) @cmd2.with_argument_list @@ -2614,13 +2594,14 @@ def do_exit(self, arg_list) -> bool: Usage: exit [exit_code] Where: - * exit_code - integer exit code to return to the shell""" + * exit_code - integer exit code to return to the shell + """ # If an argument was provided if arg_list: try: self.exit_code = int(arg_list[0]) except ValueError: - self.perror("{} isn't a valid integer exit code".format(arg_list[0])) + self.perror(f"{arg_list[0]} isn't a valid integer exit code") self.exit_code = 1 # Return True to stop the command loop @@ -2628,7 +2609,7 @@ def do_exit(self, arg_list) -> bool: def postloop(self) -> None: """Hook method executed once when the cmdloop() method is about to return.""" - self.poutput('exiting with code: {}'.format(self.exit_code)) + self.poutput(f'exiting with code: {self.exit_code}') @pytest.fixture @@ -2638,7 +2619,7 @@ def exit_code_repl(): return app -def test_exit_code_default(exit_code_repl): +def test_exit_code_default(exit_code_repl) -> None: app = exit_code_repl app.use_rawinput = True @@ -2654,7 +2635,7 @@ def test_exit_code_default(exit_code_repl): assert out == expected -def test_exit_code_nonzero(exit_code_repl): +def test_exit_code_nonzero(exit_code_repl) -> None: app = exit_code_repl app.use_rawinput = True @@ -2671,21 +2652,21 @@ def test_exit_code_nonzero(exit_code_repl): class AnsiApp(cmd2.Cmd): - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_echo(self, args): + def do_echo(self, args) -> None: self.poutput(args) self.perror(args) - def do_echo_error(self, args): + def do_echo_error(self, args) -> None: self.poutput(ansi.style(args, fg=ansi.Fg.RED)) # perror uses colors by default self.perror(args) @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_ansi_pouterr_always_tty(mocker, capsys): +def test_ansi_pouterr_always_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) @@ -2708,7 +2689,7 @@ def test_ansi_pouterr_always_tty(mocker, capsys): @with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_ansi_pouterr_always_notty(mocker, capsys): +def test_ansi_pouterr_always_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) @@ -2731,7 +2712,7 @@ def test_ansi_pouterr_always_notty(mocker, capsys): @with_ansi_style(ansi.AllowStyle.TERMINAL) -def test_ansi_terminal_tty(mocker, capsys): +def test_ansi_terminal_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) @@ -2753,7 +2734,7 @@ def test_ansi_terminal_tty(mocker, capsys): @with_ansi_style(ansi.AllowStyle.TERMINAL) -def test_ansi_terminal_notty(mocker, capsys): +def test_ansi_terminal_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) @@ -2768,7 +2749,7 @@ def test_ansi_terminal_notty(mocker, capsys): @with_ansi_style(ansi.AllowStyle.NEVER) -def test_ansi_never_tty(mocker, capsys): +def test_ansi_never_tty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) @@ -2783,7 +2764,7 @@ def test_ansi_never_tty(mocker, capsys): @with_ansi_style(ansi.AllowStyle.NEVER) -def test_ansi_never_notty(mocker, capsys): +def test_ansi_never_notty(mocker, capsys) -> None: app = AnsiApp() mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) @@ -2802,32 +2783,31 @@ class DisableCommandsApp(cmd2.Cmd): category_name = "Test Category" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @cmd2.with_category(category_name) - def do_has_helper_funcs(self, arg): + def do_has_helper_funcs(self, arg) -> None: self.poutput("The real has_helper_funcs") - def help_has_helper_funcs(self): + def help_has_helper_funcs(self) -> None: self.poutput('Help for has_helper_funcs') def complete_has_helper_funcs(self, *args): return ['result'] @cmd2.with_category(category_name) - def do_has_no_helper_funcs(self, arg): + def do_has_no_helper_funcs(self, arg) -> None: """Help for has_no_helper_funcs""" self.poutput("The real has_no_helper_funcs") @pytest.fixture def disable_commands_app(): - app = DisableCommandsApp() - return app + return DisableCommandsApp() -def test_disable_and_enable_category(disable_commands_app): +def test_disable_and_enable_category(disable_commands_app) -> None: ########################################################################## # Disable the category ########################################################################## @@ -2849,7 +2829,7 @@ def test_disable_and_enable_category(disable_commands_app): # Make sure neither function completes text = '' - line = 'has_helper_funcs {}'.format(text) + line = f'has_helper_funcs {text}' endidx = len(line) begidx = endidx - len(text) @@ -2857,7 +2837,7 @@ def test_disable_and_enable_category(disable_commands_app): assert first_match is None text = '' - line = 'has_no_helper_funcs {}'.format(text) + line = f'has_no_helper_funcs {text}' endidx = len(line) begidx = endidx - len(text) @@ -2893,16 +2873,17 @@ def test_disable_and_enable_category(disable_commands_app): # has_helper_funcs should complete now text = '' - line = 'has_helper_funcs {}'.format(text) + line = f'has_helper_funcs {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, disable_commands_app) - assert first_match is not None and disable_commands_app.completion_matches == ['result '] + assert first_match is not None + assert disable_commands_app.completion_matches == ['result '] # has_no_helper_funcs had no completer originally, so there should be no results text = '' - line = 'has_no_helper_funcs {}'.format(text) + line = f'has_no_helper_funcs {text}' endidx = len(line) begidx = endidx - len(text) @@ -2919,7 +2900,7 @@ def test_disable_and_enable_category(disable_commands_app): assert 'has_helper_funcs' in help_topics -def test_enable_enabled_command(disable_commands_app): +def test_enable_enabled_command(disable_commands_app) -> None: # Test enabling a command that is not disabled saved_len = len(disable_commands_app.disabled_commands) disable_commands_app.enable_command('has_helper_funcs') @@ -2928,12 +2909,12 @@ def test_enable_enabled_command(disable_commands_app): assert saved_len == len(disable_commands_app.disabled_commands) -def test_disable_fake_command(disable_commands_app): +def test_disable_fake_command(disable_commands_app) -> None: with pytest.raises(AttributeError): disable_commands_app.disable_command('fake', 'fake message') -def test_disable_command_twice(disable_commands_app): +def test_disable_command_twice(disable_commands_app) -> None: saved_len = len(disable_commands_app.disabled_commands) message_to_print = 'These commands are currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) @@ -2949,7 +2930,7 @@ def test_disable_command_twice(disable_commands_app): assert saved_len == new_len -def test_disabled_command_not_in_history(disable_commands_app): +def test_disabled_command_not_in_history(disable_commands_app) -> None: message_to_print = 'These commands are currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) @@ -2958,8 +2939,8 @@ def test_disabled_command_not_in_history(disable_commands_app): 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) +def test_disabled_message_command_name(disable_commands_app) -> None: + message_to_print = f'{COMMAND_NAME} is currently disabled' disable_commands_app.disable_command('has_helper_funcs', message_to_print) out, err = run_cmd(disable_commands_app, 'has_helper_funcs') @@ -2967,7 +2948,7 @@ def test_disabled_message_command_name(disable_commands_app): @pytest.mark.parametrize('silence_startup_script', [True, False]) -def test_startup_script(request, capsys, silence_startup_script): +def test_startup_script(request, capsys, silence_startup_script) -> None: test_dir = os.path.dirname(request.module.__file__) startup_script = os.path.join(test_dir, '.cmd2rc') app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script, silence_startup_script=silence_startup_script) @@ -2987,7 +2968,7 @@ def test_startup_script(request, capsys, silence_startup_script): @pytest.mark.parametrize('startup_script', odd_file_names) -def test_startup_script_with_odd_file_names(startup_script): +def test_startup_script_with_odd_file_names(startup_script) -> None: """Test file names with various patterns""" # Mock os.path.exists to trick cmd2 into adding this script to its startup commands saved_exists = os.path.exists @@ -2995,19 +2976,19 @@ def test_startup_script_with_odd_file_names(startup_script): app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script) assert len(app._startup_commands) == 1 - assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(os.path.abspath(startup_script))) + assert app._startup_commands[0] == f"run_script {utils.quote_string(os.path.abspath(startup_script))}" # Restore os.path.exists os.path.exists = saved_exists -def test_transcripts_at_init(): +def test_transcripts_at_init() -> None: transcript_files = ['foo', 'bar'] app = cmd2.Cmd(allow_cli_args=False, transcript_files=transcript_files) assert app._transcript_files == transcript_files -def test_columnize_too_wide(outsim_app): +def test_columnize_too_wide(outsim_app) -> None: """Test calling columnize with output that wider than display_width""" str_list = ["way too wide", "much wider than the first"] outsim_app.columnize(str_list, display_width=5) @@ -3016,7 +2997,7 @@ def test_columnize_too_wide(outsim_app): assert outsim_app.stdout.getvalue() == expected -def test_command_parser_retrieval(outsim_app: cmd2.Cmd): +def test_command_parser_retrieval(outsim_app: cmd2.Cmd) -> None: # Pass something that isn't a method not_a_method = "just a string" assert outsim_app._command_parsers.get(not_a_method) is None @@ -3025,7 +3006,7 @@ def test_command_parser_retrieval(outsim_app: cmd2.Cmd): assert outsim_app._command_parsers.get(outsim_app.__init__) is None -def test_command_synonym_parser(): +def test_command_synonym_parser() -> None: # Make sure a command synonym returns the same parser as what it aliases class SynonymApp(cmd2.cmd2.Cmd): do_synonym = cmd2.cmd2.Cmd.do_help diff --git a/tests/test_completion.py b/tests/test_completion.py old mode 100755 new mode 100644 index cd2a2af08..2deb6a80a --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,7 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit/functional testing for readline tab completion functions in the cmd2.py module. +"""Unit/functional testing for readline tab completion functions in the cmd2.py module. These are primarily tests related to readline completer functions which handle tab completion of cmd2/cmd commands, file system paths, and shell commands. @@ -10,6 +7,7 @@ import enum import os import sys +from typing import NoReturn from unittest import ( mock, ) @@ -59,11 +57,9 @@ class CompletionsExample(cmd2.Cmd): - """ - Example cmd2 application used to exercise tab completion tests - """ + """Example cmd2 application used to exercise tab completion tests""" - def __init__(self): + def __init__(self) -> None: cmd2.Cmd.__init__(self, multiline_commands=['test_multiline']) self.foo = 'bar' self.add_settable( @@ -76,47 +72,45 @@ def __init__(self): ) ) - def do_test_basic(self, args): + def do_test_basic(self, args) -> None: pass def complete_test_basic(self, text, line, begidx, endidx): return self.basic_complete(text, line, begidx, endidx, food_item_strs) - def do_test_delimited(self, args): + def do_test_delimited(self, args) -> None: pass def complete_test_delimited(self, text, line, begidx, endidx): return self.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/') - def do_test_sort_key(self, args): + def do_test_sort_key(self, args) -> None: pass def complete_test_sort_key(self, text, line, begidx, endidx): num_strs = ['2', '11', '1'] return self.basic_complete(text, line, begidx, endidx, num_strs) - def do_test_raise_exception(self, args): + def do_test_raise_exception(self, args) -> None: pass - def complete_test_raise_exception(self, text, line, begidx, endidx): + def complete_test_raise_exception(self, text, line, begidx, endidx) -> NoReturn: raise IndexError("You are out of bounds!!") - def do_test_multiline(self, args): + def do_test_multiline(self, args) -> None: pass def complete_test_multiline(self, text, line, begidx, endidx): return self.basic_complete(text, line, begidx, endidx, sport_item_strs) - def do_test_no_completer(self, args): + def do_test_no_completer(self, args) -> None: """Completing this should result in completedefault() being called""" - pass def complete_foo_val(self, text, line, begidx, endidx, arg_tokens): """Supports unit testing cmd2.Cmd2.complete_set_val to confirm it passes all tokens in the set command""" if 'param' in arg_tokens: return ["SUCCESS"] - else: - return ["FAIL"] + return ["FAIL"] def completedefault(self, *ignored): """Method called to complete an input line when no command-specific @@ -128,11 +122,10 @@ def completedefault(self, *ignored): @pytest.fixture def cmd2_app(): - c = CompletionsExample() - return c + return CompletionsExample() -def test_cmd2_command_completion_single(cmd2_app): +def test_cmd2_command_completion_single(cmd2_app) -> None: text = 'he' line = text endidx = len(line) @@ -140,42 +133,45 @@ def test_cmd2_command_completion_single(cmd2_app): assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] -def test_complete_command_single(cmd2_app): +def test_complete_command_single(cmd2_app) -> None: text = 'he' line = text endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == ['help '] + assert first_match is not None + assert cmd2_app.completion_matches == ['help '] -def test_complete_empty_arg(cmd2_app): +def test_complete_empty_arg(cmd2_app) -> None: text = '' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) expected = sorted(cmd2_app.get_visible_commands(), 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 + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_complete_bogus_command(cmd2_app): +def test_complete_bogus_command(cmd2_app) -> None: text = '' - line = 'fizbuzz {}'.format(text) + line = f'fizbuzz {text}' endidx = len(line) begidx = endidx - len(text) expected = ['default '] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == expected + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_complete_exception(cmd2_app, capsys): +def test_complete_exception(cmd2_app, capsys) -> None: text = '' - line = 'test_raise_exception {}'.format(text) + line = f'test_raise_exception {text}' endidx = len(line) begidx = endidx - len(text) @@ -186,7 +182,7 @@ def test_complete_exception(cmd2_app, capsys): assert "IndexError" in err -def test_complete_macro(base_app, request): +def test_complete_macro(base_app, request) -> None: # Create the macro out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}') assert out == normalize("Macro 'fake' created") @@ -195,19 +191,20 @@ def test_complete_macro(base_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') - line = 'fake {}'.format(text) + line = f'fake {text}' endidx = len(line) begidx = endidx - len(text) expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] first_match = complete_tester(text, line, begidx, endidx, base_app) - assert first_match is not None and base_app.completion_matches == expected + assert first_match is not None + assert base_app.completion_matches == expected -def test_default_sort_key(cmd2_app): +def test_default_sort_key(cmd2_app) -> None: text = '' - line = 'test_sort_key {}'.format(text) + line = f'test_sort_key {text}' endidx = len(line) begidx = endidx - len(text) @@ -215,16 +212,18 @@ def test_default_sort_key(cmd2_app): cmd2_app.default_sort_key = cmd2.Cmd.ALPHABETICAL_SORT_KEY expected = ['1', '11', '2'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == expected + assert first_match is not None + assert cmd2_app.completion_matches == expected # Now switch to natural sorting cmd2_app.default_sort_key = cmd2.Cmd.NATURAL_SORT_KEY expected = ['1', '2', '11'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == expected + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_cmd2_command_completion_multiple(cmd2_app): +def test_cmd2_command_completion_multiple(cmd2_app) -> None: text = 'h' line = text endidx = len(line) @@ -232,7 +231,7 @@ def test_cmd2_command_completion_multiple(cmd2_app): assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] -def test_cmd2_command_completion_nomatch(cmd2_app): +def test_cmd2_command_completion_nomatch(cmd2_app) -> None: text = 'fakecommand' line = text endidx = len(line) @@ -240,31 +239,33 @@ def test_cmd2_command_completion_nomatch(cmd2_app): assert cmd2_app.completenames(text, line, begidx, endidx) == [] -def test_cmd2_help_completion_single(cmd2_app): +def test_cmd2_help_completion_single(cmd2_app) -> None: text = 'he' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, 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 '] + assert first_match is not None + assert cmd2_app.completion_matches == ['help '] -def test_cmd2_help_completion_multiple(cmd2_app): +def test_cmd2_help_completion_multiple(cmd2_app) -> None: text = 'h' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == ['help', 'history'] + assert first_match is not None + assert cmd2_app.completion_matches == ['help', 'history'] -def test_cmd2_help_completion_nomatch(cmd2_app): +def test_cmd2_help_completion_nomatch(cmd2_app) -> None: text = 'fakecommand' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) @@ -272,7 +273,7 @@ def test_cmd2_help_completion_nomatch(cmd2_app): assert first_match is None -def test_set_allow_style_completion(cmd2_app): +def test_set_allow_style_completion(cmd2_app) -> None: """Confirm that completing allow_style presents AllowStyle strings""" text = '' line = 'set allow_style' @@ -286,7 +287,7 @@ def test_set_allow_style_completion(cmd2_app): assert cmd2_app.completion_matches == sorted(expected, key=cmd2_app.default_sort_key) -def test_set_bool_completion(cmd2_app): +def test_set_bool_completion(cmd2_app) -> None: """Confirm that completing a boolean Settable presents true and false strings""" text = '' line = 'set debug' @@ -300,7 +301,7 @@ def test_set_bool_completion(cmd2_app): assert cmd2_app.completion_matches == sorted(expected, key=cmd2_app.default_sort_key) -def test_shell_command_completion_shortcut(cmd2_app): +def test_shell_command_completion_shortcut(cmd2_app) -> None: # Made sure ! runs a shell command and all matches start with ! since there # isn't a space between ! and the shell command. Display matches won't # begin with the !. @@ -318,16 +319,18 @@ 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 + assert cmd2_app.completion_matches == expected + assert cmd2_app.display_matches == expected_display -def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): +def test_shell_command_completion_doesnt_match_wildcards(cmd2_app) -> None: if sys.platform == "win32": text = 'c*' else: text = 'e*' - line = 'shell {}'.format(text) + line = f'shell {text}' endidx = len(line) begidx = endidx - len(text) @@ -335,7 +338,7 @@ def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): assert first_match is None -def test_shell_command_completion_multiple(cmd2_app): +def test_shell_command_completion_multiple(cmd2_app) -> None: if sys.platform == "win32": text = 'c' expected = 'calc.exe' @@ -343,17 +346,18 @@ def test_shell_command_completion_multiple(cmd2_app): text = 'l' expected = 'ls' - line = 'shell {}'.format(text) + line = f'shell {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and expected in cmd2_app.completion_matches + assert first_match is not None + assert expected in cmd2_app.completion_matches -def test_shell_command_completion_nomatch(cmd2_app): +def test_shell_command_completion_nomatch(cmd2_app) -> None: text = 'zzzz' - line = 'shell {}'.format(text) + line = f'shell {text}' endidx = len(line) begidx = endidx - len(text) @@ -361,9 +365,9 @@ def test_shell_command_completion_nomatch(cmd2_app): assert first_match is None -def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): +def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app) -> None: text = '' - line = 'shell {}'.format(text) + line = f'shell {text}' endidx = len(line) begidx = endidx - len(text) @@ -371,24 +375,25 @@ def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): assert first_match is None -def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request): +def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'conftest') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == [text + '.py '] + assert first_match is not None + assert cmd2_app.completion_matches == [text + '.py '] -def test_shell_command_complete_in_path(cmd2_app, request): +def test_shell_command_complete_in_path(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') - line = 'shell {}'.format(text) + line = f'shell {text}' endidx = len(line) begidx = endidx - len(text) @@ -397,14 +402,15 @@ def test_shell_command_complete_in_path(cmd2_app, request): # we expect to see the scripts dir among the results expected = os.path.join(test_dir, 'scripts' + os.path.sep) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and expected in cmd2_app.completion_matches + assert first_match is not None + assert expected in cmd2_app.completion_matches -def test_path_completion_single_end(cmd2_app, request): +def test_path_completion_single_end(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'conftest') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) @@ -412,11 +418,11 @@ 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): +def test_path_completion_multiple(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) @@ -426,11 +432,11 @@ def test_path_completion_multiple(cmd2_app, request): assert matches == expected -def test_path_completion_nomatch(cmd2_app, request): +def test_path_completion_nomatch(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'fakepath') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) @@ -438,7 +444,7 @@ def test_path_completion_nomatch(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx) == [] -def test_default_to_shell_completion(cmd2_app, request): +def test_default_to_shell_completion(cmd2_app, request) -> None: cmd2_app.default_to_shell = True test_dir = os.path.dirname(request.module.__file__) @@ -451,26 +457,27 @@ def test_default_to_shell_completion(cmd2_app, request): # Make sure the command is on the testing system assert command in utils.get_exes_in_path(command) - line = '{} {}'.format(command, text) + line = f'{command} {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == [text + '.py '] + assert first_match is not None + assert cmd2_app.completion_matches == [text + '.py '] -def test_path_completion_no_text(cmd2_app): +def test_path_completion_no_text(cmd2_app) -> None: # Run path complete with no search text which should show what's in cwd text = '' - line = 'shell ls {}'.format(text) + line = f'shell ls {text}' endidx = len(line) begidx = endidx - len(text) completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx) # Run path complete with path set to the CWD text = os.getcwd() + os.path.sep - line = 'shell ls {}'.format(text) + line = f'shell ls {text}' endidx = len(line) begidx = endidx - len(text) @@ -482,17 +489,17 @@ def test_path_completion_no_text(cmd2_app): assert completions_cwd -def test_path_completion_no_path(cmd2_app): +def test_path_completion_no_path(cmd2_app) -> None: # Run path complete with search text that isn't preceded by a path. This should use CWD as the path. text = 'p' - line = 'shell ls {}'.format(text) + line = f'shell ls {text}' endidx = len(line) begidx = endidx - len(text) completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx) # Run path complete with path set to the CWD text = os.getcwd() + os.path.sep + text - line = 'shell ls {}'.format(text) + line = f'shell ls {text}' endidx = len(line) begidx = endidx - len(text) @@ -505,13 +512,13 @@ def test_path_completion_no_path(cmd2_app): @pytest.mark.skipif(sys.platform == 'win32', reason="this only applies on systems where the root directory is a slash") -def test_path_completion_cwd_is_root_dir(cmd2_app): +def test_path_completion_cwd_is_root_dir(cmd2_app) -> None: # Change our CWD to root dir cwd = os.getcwd() os.chdir(os.path.sep) text = '' - line = 'shell ls {}'.format(text) + line = f'shell ls {text}' endidx = len(line) begidx = endidx - len(text) completions = cmd2_app.path_complete(text, line, begidx, endidx) @@ -523,11 +530,11 @@ def test_path_completion_cwd_is_root_dir(cmd2_app): os.chdir(cwd) -def test_path_completion_doesnt_match_wildcards(cmd2_app, request): +def test_path_completion_doesnt_match_wildcards(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c*') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) @@ -536,13 +543,13 @@ def test_path_completion_doesnt_match_wildcards(cmd2_app, request): assert cmd2_app.path_complete(text, line, begidx, endidx) == [] -def test_path_completion_complete_user(cmd2_app): +def test_path_completion_complete_user(cmd2_app) -> None: import getpass user = getpass.getuser() - text = '~{}'.format(user) - line = 'shell fake {}'.format(text) + text = f'~{user}' + line = f'shell fake {text}' endidx = len(line) begidx = endidx - len(text) completions = cmd2_app.path_complete(text, line, begidx, endidx) @@ -551,7 +558,7 @@ def test_path_completion_complete_user(cmd2_app): assert expected in completions -def test_path_completion_user_path_expansion(cmd2_app): +def test_path_completion_user_path_expansion(cmd2_app) -> None: # Run path with a tilde and a slash if sys.platform.startswith('win'): cmd = 'dir' @@ -559,15 +566,15 @@ def test_path_completion_user_path_expansion(cmd2_app): cmd = 'ls' # Use a ~ which will be expanded into the user's home directory - text = '~{}'.format(os.path.sep) - line = 'shell {} {}'.format(cmd, text) + text = f'~{os.path.sep}' + line = f'shell {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)] # Run path complete on the user's home directory text = os.path.expanduser('~') + os.path.sep - line = 'shell {} {}'.format(cmd, text) + line = f'shell {cmd} {text}' endidx = len(line) begidx = endidx - len(text) completions_home = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] @@ -575,11 +582,11 @@ def test_path_completion_user_path_expansion(cmd2_app): assert completions_tilde_slash == completions_home -def test_path_completion_directories_only(cmd2_app, request): +def test_path_completion_directories_only(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') - line = 'shell cat {}'.format(text) + line = f'shell cat {text}' endidx = len(line) begidx = endidx - len(text) @@ -589,18 +596,18 @@ 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): +def test_basic_completion_single(cmd2_app) -> None: text = 'Pi' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] -def test_basic_completion_multiple(cmd2_app): +def test_basic_completion_multiple(cmd2_app) -> None: text = '' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) @@ -608,18 +615,18 @@ def test_basic_completion_multiple(cmd2_app): assert matches == sorted(food_item_strs) -def test_basic_completion_nomatch(cmd2_app): +def test_basic_completion_nomatch(cmd2_app) -> None: text = 'q' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == [] -def test_delimiter_completion(cmd2_app): +def test_delimiter_completion(cmd2_app) -> None: text = '/home/' - line = 'run_script {}'.format(text) + line = f'run_script {text}' endidx = len(line) begidx = endidx - len(text) @@ -632,18 +639,18 @@ def test_delimiter_completion(cmd2_app): assert display_list == ['other user', 'user'] -def test_flag_based_completion_single(cmd2_app): +def test_flag_based_completion_single(cmd2_app) -> None: text = 'Pi' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza'] -def test_flag_based_completion_multiple(cmd2_app): +def test_flag_based_completion_multiple(cmd2_app) -> None: text = '' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) @@ -651,20 +658,20 @@ def test_flag_based_completion_multiple(cmd2_app): assert matches == sorted(food_item_strs) -def test_flag_based_completion_nomatch(cmd2_app): +def test_flag_based_completion_nomatch(cmd2_app) -> None: text = 'q' - line = 'list_food -f {}'.format(text) + line = f'list_food -f {text}' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [] -def test_flag_based_default_completer(cmd2_app, request): +def test_flag_based_default_completer(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') - line = 'list_food {}'.format(text) + line = f'list_food {text}' endidx = len(line) begidx = endidx - len(text) @@ -674,11 +681,11 @@ def test_flag_based_default_completer(cmd2_app, request): ] -def test_flag_based_callable_completer(cmd2_app, request): +def test_flag_based_callable_completer(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') - line = 'list_food -o {}'.format(text) + line = f'list_food -o {text}' endidx = len(line) begidx = endidx - len(text) @@ -687,18 +694,18 @@ def test_flag_based_callable_completer(cmd2_app, request): assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [text + 'onftest.py'] -def test_index_based_completion_single(cmd2_app): +def test_index_based_completion_single(cmd2_app) -> None: text = 'Foo' - line = 'command Pizza {}'.format(text) + line = f'command Pizza {text}' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == ['Football'] -def test_index_based_completion_multiple(cmd2_app): +def test_index_based_completion_multiple(cmd2_app) -> None: text = '' - line = 'command Pizza {}'.format(text) + line = f'command Pizza {text}' endidx = len(line) begidx = endidx - len(text) @@ -706,19 +713,19 @@ def test_index_based_completion_multiple(cmd2_app): assert matches == sorted(sport_item_strs) -def test_index_based_completion_nomatch(cmd2_app): +def test_index_based_completion_nomatch(cmd2_app) -> None: text = 'q' - line = 'command {}'.format(text) + line = f'command {text}' endidx = len(line) 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): +def test_index_based_default_completer(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') - line = 'command Pizza Bat Computer {}'.format(text) + line = f'command Pizza Bat Computer {text}' endidx = len(line) begidx = endidx - len(text) @@ -728,11 +735,11 @@ def test_index_based_default_completer(cmd2_app, request): ] -def test_index_based_callable_completer(cmd2_app, request): +def test_index_based_callable_completer(cmd2_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') - line = 'command Pizza Bat {}'.format(text) + line = f'command Pizza Bat {text}' endidx = len(line) begidx = endidx - len(text) @@ -741,9 +748,9 @@ def test_index_based_callable_completer(cmd2_app, request): assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [text + 'onftest.py'] -def test_tokens_for_completion_quoted(cmd2_app): +def test_tokens_for_completion_quoted(cmd2_app) -> None: text = 'Pi' - line = 'list_food "{}"'.format(text) + line = f'list_food "{text}"' endidx = len(line) begidx = endidx @@ -755,9 +762,9 @@ def test_tokens_for_completion_quoted(cmd2_app): assert expected_raw_tokens == raw_tokens -def test_tokens_for_completion_unclosed_quote(cmd2_app): +def test_tokens_for_completion_unclosed_quote(cmd2_app) -> None: text = 'Pi' - line = 'list_food "{}'.format(text) + line = f'list_food "{text}' endidx = len(line) begidx = endidx - len(text) @@ -769,10 +776,10 @@ def test_tokens_for_completion_unclosed_quote(cmd2_app): assert expected_raw_tokens == raw_tokens -def test_tokens_for_completion_punctuation(cmd2_app): +def test_tokens_for_completion_punctuation(cmd2_app) -> None: """Test that redirectors and terminators are word delimiters""" text = 'file' - line = 'command | < ;>>{}'.format(text) + line = f'command | < ;>>{text}' endidx = len(line) begidx = endidx - len(text) @@ -784,10 +791,10 @@ def test_tokens_for_completion_punctuation(cmd2_app): assert expected_raw_tokens == raw_tokens -def test_tokens_for_completion_quoted_punctuation(cmd2_app): +def test_tokens_for_completion_quoted_punctuation(cmd2_app) -> None: """Test that quoted punctuation characters are not word delimiters""" text = '>file' - line = 'command "{}'.format(text) + line = f'command "{text}' endidx = len(line) begidx = endidx - len(text) @@ -799,75 +806,81 @@ def test_tokens_for_completion_quoted_punctuation(cmd2_app): assert expected_raw_tokens == raw_tokens -def test_add_opening_quote_basic_no_text(cmd2_app): +def test_add_opening_quote_basic_no_text(cmd2_app) -> None: text = '' - line = 'test_basic {}'.format(text) + line = f'test_basic {text}' endidx = len(line) begidx = endidx - len(text) # 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 + assert cmd2_app.completion_matches == sorted(food_item_strs, key=cmd2_app.default_sort_key) -def test_add_opening_quote_basic_nothing_added(cmd2_app): +def test_add_opening_quote_basic_nothing_added(cmd2_app) -> None: text = 'P' - line = 'test_basic {}'.format(text) + line = f'test_basic {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == ['Pizza', 'Potato'] + assert first_match is not None + assert cmd2_app.completion_matches == ['Pizza', 'Potato'] -def test_add_opening_quote_basic_quote_added(cmd2_app): +def test_add_opening_quote_basic_quote_added(cmd2_app) -> None: text = 'Ha' - line = 'test_basic {}'.format(text) + line = f'test_basic {text}' endidx = len(line) begidx = endidx - len(text) expected = sorted(['"Ham', '"Ham Sandwich'], 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 + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_add_opening_quote_basic_single_quote_added(cmd2_app): +def test_add_opening_quote_basic_single_quote_added(cmd2_app) -> None: text = 'Ch' - line = 'test_basic {}'.format(text) + line = f'test_basic {text}' endidx = len(line) begidx = endidx - len(text) expected = ["'Cheese \"Pizza\"' "] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == expected + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): +def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app) -> None: # This tests when the text entered is the same as the common prefix of the matches text = 'Ham' - line = 'test_basic {}'.format(text) + line = f'test_basic {text}' endidx = len(line) begidx = endidx - len(text) expected = sorted(['"Ham', '"Ham Sandwich'], 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 + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_add_opening_quote_delimited_no_text(cmd2_app): +def test_add_opening_quote_delimited_no_text(cmd2_app) -> None: text = '' - line = 'test_delimited {}'.format(text) + line = f'test_delimited {text}' endidx = len(line) begidx = endidx - len(text) # 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 + assert cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key) -def test_add_opening_quote_delimited_nothing_added(cmd2_app): +def test_add_opening_quote_delimited_nothing_added(cmd2_app) -> None: text = '/ho' - line = 'test_delimited {}'.format(text) + line = f'test_delimited {text}' endidx = len(line) begidx = endidx - len(text) @@ -875,16 +888,14 @@ 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 + assert cmd2_app.completion_matches == expected_matches + assert cmd2_app.display_matches == expected_display -def test_add_opening_quote_delimited_quote_added(cmd2_app): +def test_add_opening_quote_delimited_quote_added(cmd2_app) -> None: text = '/home/user/fi' - line = 'test_delimited {}'.format(text) + line = f'test_delimited {text}' endidx = len(line) begidx = endidx - len(text) @@ -892,17 +903,15 @@ 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 + assert os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + assert cmd2_app.display_matches == expected_display -def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): +def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app) -> None: # This tests when the text entered is the same as the common prefix of the matches text = '/home/user/file' - line = 'test_delimited {}'.format(text) + line = f'test_delimited {text}' endidx = len(line) begidx = endidx - len(text) @@ -910,17 +919,15 @@ 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 + assert os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + assert cmd2_app.display_matches == expected_display -def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): +def test_add_opening_quote_delimited_space_in_prefix(cmd2_app) -> None: # This test when a space appears before the part of the string that is the display match text = '/home/oth' - line = 'test_delimited {}'.format(text) + line = f'test_delimited {text}' endidx = len(line) begidx = endidx - len(text) @@ -928,60 +935,62 @@ 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 + assert os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix + assert cmd2_app.display_matches == expected_display -def test_no_completer(cmd2_app): +def test_no_completer(cmd2_app) -> None: text = '' - line = 'test_no_completer {}'.format(text) + line = f'test_no_completer {text}' endidx = len(line) begidx = endidx - len(text) expected = ['default '] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is not None and cmd2_app.completion_matches == expected + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_wordbreak_in_command(cmd2_app): +def test_wordbreak_in_command(cmd2_app) -> None: text = '' - line = '"{}'.format(text) + line = f'"{text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) - assert first_match is None and not cmd2_app.completion_matches + assert first_match is None + assert not cmd2_app.completion_matches -def test_complete_multiline_on_single_line(cmd2_app): +def test_complete_multiline_on_single_line(cmd2_app) -> None: text = '' - line = 'test_multiline {}'.format(text) + line = f'test_multiline {text}' endidx = len(line) begidx = endidx - len(text) expected = sorted(sport_item_strs, 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 + assert first_match is not None + assert cmd2_app.completion_matches == expected -def test_complete_multiline_on_multiple_lines(cmd2_app): +def test_complete_multiline_on_multiple_lines(cmd2_app) -> None: # Set the same variables _complete_statement() sets when a user is entering data at a continuation prompt cmd2_app._at_continuation_prompt = True cmd2_app._multiline_in_progress = "test_multiline\n" text = 'Ba' - line = '{}'.format(text) + line = f'{text}' endidx = len(line) begidx = endidx - len(text) expected = sorted(['Bat', 'Basket', 'Basketball'], 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 + assert first_match is not None + assert cmd2_app.completion_matches == expected # Used by redirect_complete tests @@ -993,7 +1002,7 @@ class RedirCompType(enum.Enum): @pytest.mark.parametrize( - 'line, comp_type', + ('line', 'comp_type'), [ ('fake', RedirCompType.DEFAULT), ('fake arg', RedirCompType.DEFAULT), @@ -1019,7 +1028,7 @@ class RedirCompType(enum.Enum): ('fake > file >>', RedirCompType.NONE), ], ) -def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): +def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type) -> None: # Test both cases of allow_redirection cmd2_app.allow_redirection = True for count in range(2): @@ -1032,7 +1041,7 @@ def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): default_complete_mock = mock.MagicMock(name='fake_completer') text = '' - line = '{} {}'.format(line, text) + line = f'{line} {text}' endidx = len(line) begidx = endidx - len(text) @@ -1055,9 +1064,9 @@ def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type): comp_type = RedirCompType.NONE -def test_complete_set_value(cmd2_app): +def test_complete_set_value(cmd2_app) -> None: text = '' - line = 'set foo {}'.format(text) + line = f'set foo {text}' endidx = len(line) begidx = endidx - len(text) @@ -1066,9 +1075,9 @@ def test_complete_set_value(cmd2_app): assert cmd2_app.completion_hint == "Hint:\n value a settable param\n" -def test_complete_set_value_invalid_settable(cmd2_app, capsys): +def test_complete_set_value_invalid_settable(cmd2_app, capsys) -> None: text = '' - line = 'set fake {}'.format(text) + line = f'set fake {text}' endidx = len(line) begidx = endidx - len(text) @@ -1086,31 +1095,33 @@ def sc_app(): return c -def test_cmd2_subcommand_completion_single_end(sc_app): +def test_cmd2_subcommand_completion_single_end(sc_app) -> None: text = 'f' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, 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 '] + assert first_match is not None + assert sc_app.completion_matches == ['foo '] -def test_cmd2_subcommand_completion_multiple(sc_app): +def test_cmd2_subcommand_completion_multiple(sc_app) -> None: text = '' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) - assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport'] + assert first_match is not None + assert sc_app.completion_matches == ['bar', 'foo', 'sport'] -def test_cmd2_subcommand_completion_nomatch(sc_app): +def test_cmd2_subcommand_completion_nomatch(sc_app) -> None: text = 'z' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) @@ -1118,31 +1129,33 @@ def test_cmd2_subcommand_completion_nomatch(sc_app): assert first_match is None -def test_help_subcommand_completion_single(sc_app): +def test_help_subcommand_completion_single(sc_app) -> None: text = 'base' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, 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 '] + assert first_match is not None + assert sc_app.completion_matches == ['base '] -def test_help_subcommand_completion_multiple(sc_app): +def test_help_subcommand_completion_multiple(sc_app) -> None: text = '' - line = 'help base {}'.format(text) + line = f'help base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) - assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport'] + assert first_match is not None + assert sc_app.completion_matches == ['bar', 'foo', 'sport'] -def test_help_subcommand_completion_nomatch(sc_app): +def test_help_subcommand_completion_nomatch(sc_app) -> None: text = 'z' - line = 'help base {}'.format(text) + line = f'help base {text}' endidx = len(line) begidx = endidx - len(text) @@ -1150,24 +1163,25 @@ def test_help_subcommand_completion_nomatch(sc_app): assert first_match is None -def test_subcommand_tab_completion(sc_app): +def test_subcommand_tab_completion(sc_app) -> None: # This makes sure the correct completer for the sport subcommand is called text = 'Foot' - line = 'base sport {}'.format(text) + line = f'base sport {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) # It is at end of line, so extra space is present - assert first_match is not None and sc_app.completion_matches == ['Football '] + assert first_match is not None + assert sc_app.completion_matches == ['Football '] -def test_subcommand_tab_completion_with_no_completer(sc_app): +def test_subcommand_tab_completion_with_no_completer(sc_app) -> None: # This tests what happens when a subcommand has no completer # In this case, the foo subcommand has no completer defined text = 'Foot' - line = 'base foo {}'.format(text) + line = f'base foo {text}' endidx = len(line) begidx = endidx - len(text) @@ -1175,41 +1189,42 @@ def test_subcommand_tab_completion_with_no_completer(sc_app): assert first_match is None -def test_subcommand_tab_completion_space_in_text(sc_app): +def test_subcommand_tab_completion_space_in_text(sc_app) -> None: text = 'B' - line = 'base sport "Space {}'.format(text) + line = f'base sport "Space {text}' endidx = len(line) begidx = endidx - len(text) 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 + assert sc_app.completion_matches == ['Ball" '] + assert sc_app.display_matches == ['Space Ball'] #################################################### class SubcommandsWithUnknownExample(cmd2.Cmd): - """ - Example cmd2 application where we a base command which has a couple subcommands + """Example cmd2 application where we a base command which has a couple subcommands and the "sport" subcommand has tab completion enabled. """ - def __init__(self): + def __init__(self) -> None: cmd2.Cmd.__init__(self) # subcommand functions for the base command - def base_foo(self, args): - """foo subcommand of base command""" + def base_foo(self, args) -> None: + """Foo subcommand of base command""" self.poutput(args.x * args.y) - def base_bar(self, args): - """bar subcommand of base command""" - self.poutput('((%s))' % args.z) + def base_bar(self, args) -> None: + """Bar subcommand of base command""" + self.poutput(f'(({args.z}))') - def base_sport(self, args): - """sport subcommand of base command""" - self.poutput('Sport is {}'.format(args.sport)) + def base_sport(self, args) -> None: + """Sport subcommand of base command""" + self.poutput(f'Sport is {args.sport}') # create the top-level parser for the base command base_parser = cmd2.Cmd2ArgumentParser() @@ -1231,7 +1246,7 @@ def base_sport(self, args): sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) @cmd2.with_argparser(base_parser, with_unknown_args=True) - def do_base(self, args): + def do_base(self, args) -> None: """Base command help""" func = getattr(args, 'func', None) if func is not None: @@ -1245,37 +1260,38 @@ def do_base(self, args): @pytest.fixture def scu_app(): """Declare test fixture for with_argparser decorator""" - app = SubcommandsWithUnknownExample() - return app + return SubcommandsWithUnknownExample() -def test_subcmd_with_unknown_completion_single_end(scu_app): +def test_subcmd_with_unknown_completion_single_end(scu_app) -> None: text = 'f' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) - print('first_match: {}'.format(first_match)) + print(f'first_match: {first_match}') # It is at end of line, so extra space is present - assert first_match is not None and scu_app.completion_matches == ['foo '] + assert first_match is not None + assert scu_app.completion_matches == ['foo '] -def test_subcmd_with_unknown_completion_multiple(scu_app): +def test_subcmd_with_unknown_completion_multiple(scu_app) -> None: text = '' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) - assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + assert first_match is not None + assert scu_app.completion_matches == ['bar', 'foo', 'sport'] -def test_subcmd_with_unknown_completion_nomatch(scu_app): +def test_subcmd_with_unknown_completion_nomatch(scu_app) -> None: text = 'z' - line = 'base {}'.format(text) + line = f'base {text}' endidx = len(line) begidx = endidx - len(text) @@ -1283,51 +1299,55 @@ def test_subcmd_with_unknown_completion_nomatch(scu_app): assert first_match is None -def test_help_subcommand_completion_single_scu(scu_app): +def test_help_subcommand_completion_single_scu(scu_app) -> None: text = 'base' - line = 'help {}'.format(text) + line = f'help {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) # It is at end of line, so extra space is present - assert first_match is not None and scu_app.completion_matches == ['base '] + assert first_match is not None + assert scu_app.completion_matches == ['base '] -def test_help_subcommand_completion_multiple_scu(scu_app): +def test_help_subcommand_completion_multiple_scu(scu_app) -> None: text = '' - line = 'help base {}'.format(text) + line = f'help base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) - assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + assert first_match is not None + assert scu_app.completion_matches == ['bar', 'foo', 'sport'] -def test_help_subcommand_completion_with_flags_before_command(scu_app): +def test_help_subcommand_completion_with_flags_before_command(scu_app) -> None: text = '' - line = 'help -h -v base {}'.format(text) + line = f'help -h -v base {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) - assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] + assert first_match is not None + assert scu_app.completion_matches == ['bar', 'foo', 'sport'] -def test_complete_help_subcommands_with_blank_command(scu_app): +def test_complete_help_subcommands_with_blank_command(scu_app) -> None: text = '' - line = 'help "" {}'.format(text) + line = f'help "" {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) - assert first_match is None and not scu_app.completion_matches + assert first_match is None + assert not scu_app.completion_matches -def test_help_subcommand_completion_nomatch_scu(scu_app): +def test_help_subcommand_completion_nomatch_scu(scu_app) -> None: text = 'z' - line = 'help base {}'.format(text) + line = f'help base {text}' endidx = len(line) begidx = endidx - len(text) @@ -1335,24 +1355,25 @@ def test_help_subcommand_completion_nomatch_scu(scu_app): assert first_match is None -def test_subcommand_tab_completion_scu(scu_app): +def test_subcommand_tab_completion_scu(scu_app) -> None: # This makes sure the correct completer for the sport subcommand is called text = 'Foot' - line = 'base sport {}'.format(text) + line = f'base sport {text}' endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, scu_app) # It is at end of line, so extra space is present - assert first_match is not None and scu_app.completion_matches == ['Football '] + assert first_match is not None + assert scu_app.completion_matches == ['Football '] -def test_subcommand_tab_completion_with_no_completer_scu(scu_app): +def test_subcommand_tab_completion_with_no_completer_scu(scu_app) -> None: # This tests what happens when a subcommand has no completer # In this case, the foo subcommand has no completer defined text = 'Foot' - line = 'base foo {}'.format(text) + line = f'base foo {text}' endidx = len(line) begidx = endidx - len(text) @@ -1360,12 +1381,14 @@ def test_subcommand_tab_completion_with_no_completer_scu(scu_app): assert first_match is None -def test_subcommand_tab_completion_space_in_text_scu(scu_app): +def test_subcommand_tab_completion_space_in_text_scu(scu_app) -> None: text = 'B' - line = 'base sport "Space {}'.format(text) + line = f'base sport "Space {text}' endidx = len(line) begidx = endidx - len(text) 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 + assert scu_app.completion_matches == ['Ball" '] + assert scu_app.display_matches == ['Space Ball'] diff --git a/tests/test_history.py b/tests/test_history.py old mode 100755 new mode 100644 index 284112d23..7b2a3a7c6 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -1,9 +1,6 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Test history functions of cmd2 -""" +"""Test history functions of cmd2""" +import contextlib import os import tempfile from unittest import ( @@ -34,7 +31,7 @@ def verify_hi_last_result(app: cmd2.Cmd, expected_length: int) -> None: # # readline tests # -def test_readline_remove_history_item(): +def test_readline_remove_history_item() -> None: from cmd2.rl_utils import ( readline, ) @@ -60,7 +57,7 @@ def hist(): Statement, ) - h = History( + return History( [ HistoryItem(Statement('', raw='first')), HistoryItem(Statement('', raw='second')), @@ -68,7 +65,6 @@ def hist(): HistoryItem(Statement('', raw='fourth')), ] ) - return h # Represents the hist fixture's JSON @@ -161,7 +157,7 @@ def persisted_hist(): return h -def test_history_class_span(hist): +def test_history_class_span(hist) -> None: span = hist.span('2..') assert len(span) == 3 assert span[2].statement.raw == 'second' @@ -227,12 +223,13 @@ def test_history_class_span(hist): assert span[3].statement.raw == 'third' value_errors = ['fred', 'fred:joe', '2', '-2', 'a..b', '2 ..', '1 : 3', '1:0', '0:3'] + expected_err = "History indices must be positive or negative integers, and may not be zero." for tryit in value_errors: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=expected_err): hist.span(tryit) -def test_persisted_history_span(persisted_hist): +def test_persisted_history_span(persisted_hist) -> None: span = persisted_hist.span('2..') assert len(span) == 5 assert span[2].statement.raw == 'second' @@ -277,12 +274,13 @@ def test_persisted_history_span(persisted_hist): assert span[5].statement.raw == 'fifth' value_errors = ['fred', 'fred:joe', '2', '-2', 'a..b', '2 ..', '1 : 3', '1:0', '0:3'] + expected_err = "History indices must be positive or negative integers, and may not be zero." for tryit in value_errors: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=expected_err): persisted_hist.span(tryit) -def test_history_class_get(hist): +def test_history_class_get(hist) -> None: assert hist.get(1).statement.raw == 'first' assert hist.get(3).statement.raw == 'third' assert hist.get(-2) == hist[-2] @@ -295,7 +293,7 @@ def test_history_class_get(hist): hist.get(5) -def test_history_str_search(hist): +def test_history_str_search(hist) -> None: items = hist.str_search('ir') assert len(items) == 2 assert items[1].statement.raw == 'first' @@ -306,7 +304,7 @@ def test_history_str_search(hist): assert items[4].statement.raw == 'fourth' -def test_history_regex_search(hist): +def test_history_regex_search(hist) -> None: items = hist.regex_search('/i.*d/') assert len(items) == 1 assert items[3].statement.raw == 'third' @@ -316,28 +314,28 @@ def test_history_regex_search(hist): assert items[2].statement.raw == 'second' -def test_history_max_length_zero(hist): +def test_history_max_length_zero(hist) -> None: hist.truncate(0) assert len(hist) == 0 -def test_history_max_length_negative(hist): +def test_history_max_length_negative(hist) -> None: hist.truncate(-1) assert len(hist) == 0 -def test_history_max_length(hist): +def test_history_max_length(hist) -> None: hist.truncate(2) assert len(hist) == 2 assert hist.get(1).statement.raw == 'third' assert hist.get(2).statement.raw == 'fourth' -def test_history_to_json(hist): +def test_history_to_json(hist) -> None: assert hist_json == hist.to_json() -def test_history_from_json(hist): +def test_history_from_json(hist) -> None: import json from cmd2.history import ( @@ -360,7 +358,8 @@ def test_history_from_json(hist): invalid_ver_json = hist.to_json() History._history_version = backed_up_ver - with pytest.raises(ValueError): + expected_err = "Unsupported history file version: BAD_VERSION. This application uses version 1.0.0." + with pytest.raises(ValueError, match=expected_err): hist.from_json(invalid_ver_json) @@ -382,8 +381,7 @@ def histitem(): command='help', arg_list=['history'], ) - histitem = HistoryItem(statement) - return histitem + return HistoryItem(statement) @pytest.fixture @@ -392,7 +390,7 @@ def parser(): StatementParser, ) - parser = StatementParser( + return StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], aliases={ @@ -404,10 +402,9 @@ def parser(): }, shortcuts={'?': 'help', '!': 'shell'}, ) - return parser -def test_multiline_histitem(parser): +def test_multiline_histitem(parser) -> None: from cmd2.history import ( History, ) @@ -423,7 +420,7 @@ def test_multiline_histitem(parser): assert pr_lines[0].endswith('multiline foo bar') -def test_multiline_with_quotes_histitem(parser): +def test_multiline_with_quotes_histitem(parser) -> None: # Test that spaces and newlines in quotes are preserved from cmd2.history import ( History, @@ -444,7 +441,7 @@ def test_multiline_with_quotes_histitem(parser): assert pr_lines[2] == ' " in quotes.;' -def test_multiline_histitem_verbose(parser): +def test_multiline_histitem_verbose(parser) -> None: from cmd2.history import ( History, ) @@ -461,7 +458,7 @@ def test_multiline_histitem_verbose(parser): assert pr_lines[1] == 'bar' -def test_single_line_format_blank(parser): +def test_single_line_format_blank(parser) -> None: from cmd2.history import ( single_line_format, ) @@ -471,7 +468,7 @@ def test_single_line_format_blank(parser): assert single_line_format(statement) == line -def test_history_item_instantiate(): +def test_history_item_instantiate() -> None: from cmd2.history import ( HistoryItem, ) @@ -489,7 +486,7 @@ def test_history_item_instantiate(): _ = HistoryItem() -def test_history_item_properties(histitem): +def test_history_item_properties(histitem) -> None: assert histitem.raw == 'help history' assert histitem.expanded == 'help history' assert str(histitem) == 'help history' @@ -498,7 +495,7 @@ def test_history_item_properties(histitem): # # test history command # -def test_base_history(base_app): +def test_base_history(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history') @@ -529,7 +526,7 @@ def test_base_history(base_app): verify_hi_last_result(base_app, 1) -def test_history_script_format(base_app): +def test_history_script_format(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history -s') @@ -543,7 +540,7 @@ def test_history_script_format(base_app): verify_hi_last_result(base_app, 2) -def test_history_with_string_argument(base_app): +def test_history_with_string_argument(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') @@ -558,7 +555,7 @@ def test_history_with_string_argument(base_app): verify_hi_last_result(base_app, 2) -def test_history_expanded_with_string_argument(base_app): +def test_history_expanded_with_string_argument(base_app) -> None: run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') @@ -575,7 +572,7 @@ def test_history_expanded_with_string_argument(base_app): verify_hi_last_result(base_app, 2) -def test_history_expanded_with_regex_argument(base_app): +def test_history_expanded_with_regex_argument(base_app) -> None: run_cmd(base_app, 'alias create sc shortcuts') run_cmd(base_app, 'help') run_cmd(base_app, 'help history') @@ -592,7 +589,7 @@ def test_history_expanded_with_regex_argument(base_app): verify_hi_last_result(base_app, 2) -def test_history_with_integer_argument(base_app): +def test_history_with_integer_argument(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history 1') @@ -605,7 +602,7 @@ def test_history_with_integer_argument(base_app): verify_hi_last_result(base_app, 1) -def test_history_with_integer_span(base_app): +def test_history_with_integer_span(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') @@ -620,7 +617,7 @@ def test_history_with_integer_span(base_app): verify_hi_last_result(base_app, 2) -def test_history_with_span_start(base_app): +def test_history_with_span_start(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') @@ -635,7 +632,7 @@ def test_history_with_span_start(base_app): verify_hi_last_result(base_app, 2) -def test_history_with_span_end(base_app): +def test_history_with_span_end(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') @@ -650,15 +647,16 @@ def test_history_with_span_end(base_app): verify_hi_last_result(base_app, 2) -def test_history_with_span_index_error(base_app): +def test_history_with_span_index_error(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, '!ls -hal :') - with pytest.raises(ValueError): + expected_err = "History indices must be positive or negative integers, and may not be zero." + with pytest.raises(ValueError, match=expected_err): base_app.onecmd('history "hal :"') -def test_history_output_file(): +def test_history_output_file() -> None: app = cmd2.Cmd(multiline_commands=['alias']) run_cmd(app, 'help') run_cmd(app, 'shortcuts') @@ -667,29 +665,29 @@ def test_history_output_file(): fd, fname = tempfile.mkstemp(prefix='', suffix='.txt') os.close(fd) - run_cmd(app, 'history -o "{}"'.format(fname)) + run_cmd(app, f'history -o "{fname}"') assert app.last_result is True - expected = normalize('\n'.join(['help', 'shortcuts', 'help history', 'alias create my_alias history;'])) + expected = normalize('help\nshortcuts\nhelp history\nalias create my_alias history;') with open(fname) as f: content = normalize(f.read()) assert content == expected -def test_history_bad_output_file(base_app): +def test_history_bad_output_file(base_app) -> None: run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') fname = os.path.join(os.path.sep, "fake", "fake", "fake") - out, err = run_cmd(base_app, 'history -o "{}"'.format(fname)) + out, err = run_cmd(base_app, f'history -o "{fname}"') assert not out assert "Error saving" in err[0] assert base_app.last_result is False -def test_history_edit(monkeypatch): +def test_history_edit(monkeypatch) -> None: app = cmd2.Cmd(multiline_commands=['alias']) # Set a fake editor just to make sure we have one. We aren't really @@ -715,7 +713,7 @@ def test_history_edit(monkeypatch): run_script_mock.assert_called_once() -def test_history_run_all_commands(base_app): +def test_history_run_all_commands(base_app) -> None: # make sure we refuse to run all commands as a default run_cmd(base_app, 'shortcuts') out, err = run_cmd(base_app, 'history -r') @@ -725,14 +723,14 @@ def test_history_run_all_commands(base_app): assert base_app.last_result is False -def test_history_run_one_command(base_app): +def test_history_run_one_command(base_app) -> None: out1, err1 = run_cmd(base_app, 'help') out2, err2 = run_cmd(base_app, 'history -r 1') assert out1 == out2 assert base_app.last_result is True -def test_history_clear(mocker, hist_file): +def test_history_clear(mocker, hist_file) -> None: # Add commands to history app = cmd2.Cmd(persistent_history_file=hist_file) run_cmd(app, 'help') @@ -767,7 +765,7 @@ def test_history_clear(mocker, hist_file): assert app.last_result is False -def test_history_verbose_with_other_options(base_app): +def test_history_verbose_with_other_options(base_app) -> None: # make sure -v shows a usage error if any other options are present options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x'] for opt in options_to_test: @@ -778,7 +776,7 @@ def test_history_verbose_with_other_options(base_app): assert base_app.last_result is False -def test_history_verbose(base_app): +def test_history_verbose(base_app) -> None: # validate function of -v option run_cmd(base_app, 'alias create s shortcuts') run_cmd(base_app, 's') @@ -795,7 +793,7 @@ def test_history_verbose(base_app): verify_hi_last_result(base_app, 2) -def test_history_script_with_invalid_options(base_app): +def test_history_script_with_invalid_options(base_app) -> None: # 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'] for opt in options_to_test: @@ -806,7 +804,7 @@ def test_history_script_with_invalid_options(base_app): assert base_app.last_result is False -def test_history_script(base_app): +def test_history_script(base_app) -> None: cmds = ['alias create s shortcuts', 's'] for cmd in cmds: run_cmd(base_app, cmd) @@ -815,7 +813,7 @@ def test_history_script(base_app): verify_hi_last_result(base_app, 2) -def test_history_expanded_with_invalid_options(base_app): +def test_history_expanded_with_invalid_options(base_app) -> None: # 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'] for opt in options_to_test: @@ -826,7 +824,7 @@ def test_history_expanded_with_invalid_options(base_app): assert base_app.last_result is False -def test_history_expanded(base_app): +def test_history_expanded(base_app) -> None: # validate function of -x option cmds = ['alias create s shortcuts', 's'] for cmd in cmds: @@ -837,7 +835,7 @@ def test_history_expanded(base_app): verify_hi_last_result(base_app, 2) -def test_history_script_expanded(base_app): +def test_history_script_expanded(base_app) -> None: # validate function of -s -x options together cmds = ['alias create s shortcuts', 's'] for cmd in cmds: @@ -848,12 +846,12 @@ def test_history_script_expanded(base_app): verify_hi_last_result(base_app, 2) -def test_base_help_history(base_app): +def test_base_help_history(base_app) -> None: out, err = run_cmd(base_app, 'help history') assert out == normalize(HELP_HISTORY) -def test_exclude_from_history(base_app): +def test_exclude_from_history(base_app) -> None: # Run history command run_cmd(base_app, 'history') verify_hi_last_result(base_app, 0) @@ -882,13 +880,11 @@ def hist_file(): os.close(fd) yield filename # teardown code - try: + with contextlib.suppress(FileNotFoundError): os.remove(filename) - except FileNotFoundError: - pass -def test_history_file_is_directory(capsys): +def test_history_file_is_directory(capsys) -> None: with tempfile.TemporaryDirectory() as test_dir: # Create a new cmd2 app cmd2.Cmd(persistent_history_file=test_dir) @@ -896,7 +892,7 @@ def test_history_file_is_directory(capsys): assert 'is a directory' in err -def test_history_can_create_directory(mocker): +def test_history_can_create_directory(mocker) -> None: # 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. mocker.patch('atexit.register') @@ -918,7 +914,7 @@ def test_history_can_create_directory(mocker): os.rmdir(hist_file_dir) -def test_history_cannot_create_directory(mocker, capsys): +def test_history_cannot_create_directory(mocker, capsys) -> None: mock_open = mocker.patch('os.makedirs') mock_open.side_effect = OSError @@ -928,7 +924,7 @@ def test_history_cannot_create_directory(mocker, capsys): assert 'Error creating persistent history file directory' in err -def test_history_file_permission_error(mocker, capsys): +def test_history_file_permission_error(mocker, capsys) -> None: mock_open = mocker.patch('builtins.open') mock_open.side_effect = PermissionError @@ -938,7 +934,7 @@ def test_history_file_permission_error(mocker, capsys): assert 'Cannot read persistent history file' in err -def test_history_file_bad_compression(mocker, capsys): +def test_history_file_bad_compression(mocker, capsys) -> None: history_file = '/tmp/doesntmatter' with open(history_file, "wb") as f: f.write(b"THIS IS NOT COMPRESSED DATA") @@ -949,7 +945,7 @@ def test_history_file_bad_compression(mocker, capsys): assert 'Error decompressing persistent history data' in err -def test_history_file_bad_json(mocker, capsys): +def test_history_file_bad_json(mocker, capsys) -> None: import lzma data = b"THIS IS NOT JSON" @@ -965,7 +961,7 @@ def test_history_file_bad_json(mocker, capsys): assert 'Error processing persistent history data' in err -def test_history_populates_readline(hist_file): +def test_history_populates_readline(hist_file) -> None: # - create a cmd2 with persistent history app = cmd2.Cmd(persistent_history_file=hist_file) run_cmd(app, 'help') @@ -1002,7 +998,7 @@ def test_history_populates_readline(hist_file): # we assume that the atexit module will call this method # properly # -def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys): +def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys) -> None: # make sure if there is no persistent history file and someone # calls the private method call that we don't get an error base_app._persist_history() @@ -1011,7 +1007,7 @@ def test_persist_history_ensure_no_error_if_no_histfile(base_app, capsys): assert not err -def test_persist_history_permission_error(hist_file, mocker, capsys): +def test_persist_history_permission_error(hist_file, mocker, capsys) -> None: app = cmd2.Cmd(persistent_history_file=hist_file) run_cmd(app, 'help') mock_open = mocker.patch('builtins.open') diff --git a/tests/test_parsing.py b/tests/test_parsing.py old mode 100755 new mode 100644 index e3d42d7c7..711868cad --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Test the parsing logic in parsing.py -""" +"""Test the parsing logic in parsing.py""" import dataclasses @@ -23,7 +19,7 @@ @pytest.fixture def parser(): - parser = StatementParser( + return StatementParser( terminators=[';', '&'], multiline_commands=['multiline'], aliases={ @@ -35,16 +31,14 @@ def parser(): }, shortcuts={'?': 'help', '!': 'shell'}, ) - return parser @pytest.fixture def default_parser(): - parser = StatementParser() - return parser + return StatementParser() -def test_parse_empty_string(parser): +def test_parse_empty_string(parser) -> None: line = '' statement = parser.parse(line) assert statement == '' @@ -62,7 +56,7 @@ def test_parse_empty_string(parser): assert statement.argv == statement.arg_list -def test_parse_empty_string_default(default_parser): +def test_parse_empty_string_default(default_parser) -> None: line = '' statement = default_parser.parse(line) assert statement == '' @@ -81,7 +75,7 @@ def test_parse_empty_string_default(default_parser): @pytest.mark.parametrize( - 'line,tokens', + ('line', 'tokens'), [ ('command', ['command']), (constants.COMMENT_CHAR + 'comment', []), @@ -93,13 +87,13 @@ def test_parse_empty_string_default(default_parser): ('help|less', ['help', '|', 'less']), ], ) -def test_tokenize_default(default_parser, line, tokens): +def test_tokenize_default(default_parser, line, tokens) -> None: tokens_to_test = default_parser.tokenize(line) assert tokens_to_test == tokens @pytest.mark.parametrize( - 'line,tokens', + ('line', 'tokens'), [ ('command', ['command']), ('# comment', []), @@ -114,20 +108,21 @@ def test_tokenize_default(default_parser, line, tokens): ('l|less', ['shell', 'ls', '-al', '|', 'less']), ], ) -def test_tokenize(parser, line, tokens): +def test_tokenize(parser, line, tokens) -> None: tokens_to_test = parser.tokenize(line) assert tokens_to_test == tokens -def test_tokenize_unclosed_quotes(parser): +def test_tokenize_unclosed_quotes(parser) -> None: with pytest.raises(exceptions.Cmd2ShlexError): _ = parser.tokenize('command with "unclosed quotes') @pytest.mark.parametrize( - 'tokens,command,args', [([], '', ''), (['command'], 'command', ''), (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')] + ('tokens', 'command', 'args'), + [([], '', ''), (['command'], 'command', ''), (['command', 'arg1', 'arg2'], 'command', 'arg1 arg2')], ) -def test_command_and_args(parser, tokens, command, args): +def test_command_and_args(parser, tokens, command, args) -> None: (parsed_command, parsed_args) = parser._command_and_args(tokens) assert command == parsed_command assert args == parsed_args @@ -141,7 +136,7 @@ def test_command_and_args(parser, tokens, command, args): "'one word'", ], ) -def test_parse_single_word(parser, line): +def test_parse_single_word(parser, line) -> None: statement = parser.parse(line) assert statement.command == line assert statement == '' @@ -159,7 +154,7 @@ def test_parse_single_word(parser, line): @pytest.mark.parametrize( - 'line,terminator', + ('line', 'terminator'), [ ('termbare;', ';'), ('termbare ;', ';'), @@ -167,7 +162,7 @@ def test_parse_single_word(parser, line): ('termbare &', '&'), ], ) -def test_parse_word_plus_terminator(parser, line, terminator): +def test_parse_word_plus_terminator(parser, line, terminator) -> None: statement = parser.parse(line) assert statement.command == 'termbare' assert statement == '' @@ -178,7 +173,7 @@ def test_parse_word_plus_terminator(parser, line, terminator): @pytest.mark.parametrize( - 'line,terminator', + ('line', 'terminator'), [ ('termbare; suffx', ';'), ('termbare ;suffx', ';'), @@ -186,7 +181,7 @@ def test_parse_word_plus_terminator(parser, line, terminator): ('termbare &suffx', '&'), ], ) -def test_parse_suffix_after_terminator(parser, line, terminator): +def test_parse_suffix_after_terminator(parser, line, terminator) -> None: statement = parser.parse(line) assert statement.command == 'termbare' assert statement == '' @@ -198,7 +193,7 @@ def test_parse_suffix_after_terminator(parser, line, terminator): assert statement.expanded_command_line == statement.command + statement.terminator + ' ' + statement.suffix -def test_parse_command_with_args(parser): +def test_parse_command_with_args(parser) -> None: line = 'command with args' statement = parser.parse(line) assert statement.command == 'command' @@ -208,7 +203,7 @@ def test_parse_command_with_args(parser): assert statement.arg_list == statement.argv[1:] -def test_parse_command_with_quoted_args(parser): +def test_parse_command_with_quoted_args(parser) -> None: line = 'command with "quoted args" and "some not"' statement = parser.parse(line) assert statement.command == 'command' @@ -218,7 +213,7 @@ def test_parse_command_with_quoted_args(parser): assert statement.arg_list == ['with', '"quoted args"', 'and', '"some not"'] -def test_parse_command_with_args_terminator_and_suffix(parser): +def test_parse_command_with_args_terminator_and_suffix(parser) -> None: line = 'command with args and terminator; and suffix' statement = parser.parse(line) assert statement.command == 'command' @@ -230,7 +225,7 @@ def test_parse_command_with_args_terminator_and_suffix(parser): assert statement.suffix == 'and suffix' -def test_parse_comment(parser): +def test_parse_comment(parser) -> None: statement = parser.parse(constants.COMMENT_CHAR + ' this is all a comment') assert statement.command == '' assert statement == '' @@ -239,7 +234,7 @@ def test_parse_comment(parser): assert not statement.arg_list -def test_parse_embedded_comment_char(parser): +def test_parse_embedded_comment_char(parser) -> None: command_str = 'hi ' + constants.COMMENT_CHAR + ' not a comment' statement = parser.parse(command_str) assert statement.command == 'hi' @@ -256,7 +251,7 @@ def test_parse_embedded_comment_char(parser): 'simple|piped', ], ) -def test_parse_simple_pipe(parser, line): +def test_parse_simple_pipe(parser, line) -> None: statement = parser.parse(line) assert statement.command == 'simple' assert statement == '' @@ -267,7 +262,7 @@ def test_parse_simple_pipe(parser, line): assert statement.expanded_command_line == statement.command + ' | ' + statement.pipe_to -def test_parse_double_pipe_is_not_a_pipe(parser): +def test_parse_double_pipe_is_not_a_pipe(parser) -> None: line = 'double-pipe || is not a pipe' statement = parser.parse(line) assert statement.command == 'double-pipe' @@ -278,7 +273,7 @@ def test_parse_double_pipe_is_not_a_pipe(parser): assert not statement.pipe_to -def test_parse_complex_pipe(parser): +def test_parse_complex_pipe(parser) -> None: line = 'command with args, terminator&sufx | piped' statement = parser.parse(line) assert statement.command == 'command' @@ -292,7 +287,7 @@ def test_parse_complex_pipe(parser): @pytest.mark.parametrize( - 'line,output', + ('line', 'output'), [ ('help > out.txt', '>'), ('help>out.txt', '>'), @@ -300,7 +295,7 @@ def test_parse_complex_pipe(parser): ('help>>out.txt', '>>'), ], ) -def test_parse_redirect(parser, line, output): +def test_parse_redirect(parser, line, output) -> None: statement = parser.parse(line) assert statement.command == 'help' assert statement == '' @@ -317,8 +312,8 @@ def test_parse_redirect(parser, line, output): 'python-cmd2/afile.txt', ], ) # without dashes # with dashes in path -def test_parse_redirect_with_args(parser, dest): - line = 'output into > {}'.format(dest) +def test_parse_redirect_with_args(parser, dest) -> None: + line = f'output into > {dest}' statement = parser.parse(line) assert statement.command == 'output' assert statement == 'into' @@ -329,7 +324,7 @@ def test_parse_redirect_with_args(parser, dest): assert statement.output_to == dest -def test_parse_redirect_append(parser): +def test_parse_redirect_append(parser) -> None: line = 'output appended to >> /tmp/afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -341,7 +336,7 @@ def test_parse_redirect_append(parser): assert statement.output_to == '/tmp/afile.txt' -def test_parse_pipe_then_redirect(parser): +def test_parse_pipe_then_redirect(parser) -> None: line = 'output into;sufx | pipethrume plz > afile.txt' statement = parser.parse(line) assert statement.command == 'output' @@ -356,7 +351,7 @@ def test_parse_pipe_then_redirect(parser): assert statement.output_to == '' -def test_parse_multiple_pipes(parser): +def test_parse_multiple_pipes(parser) -> None: line = 'output into;sufx | pipethrume plz | grep blah' statement = parser.parse(line) assert statement.command == 'output' @@ -371,7 +366,7 @@ def test_parse_multiple_pipes(parser): assert statement.output_to == '' -def test_redirect_then_pipe(parser): +def test_redirect_then_pipe(parser) -> None: line = 'help alias > file.txt | grep blah' statement = parser.parse(line) assert statement.command == 'help' @@ -386,7 +381,7 @@ def test_redirect_then_pipe(parser): assert statement.output_to == 'file.txt' -def test_append_then_pipe(parser): +def test_append_then_pipe(parser) -> None: line = 'help alias >> file.txt | grep blah' statement = parser.parse(line) assert statement.command == 'help' @@ -401,7 +396,7 @@ def test_append_then_pipe(parser): assert statement.output_to == 'file.txt' -def test_append_then_redirect(parser): +def test_append_then_redirect(parser) -> None: line = 'help alias >> file.txt > file2.txt' statement = parser.parse(line) assert statement.command == 'help' @@ -416,7 +411,7 @@ def test_append_then_redirect(parser): assert statement.output_to == 'file.txt' -def test_redirect_then_append(parser): +def test_redirect_then_append(parser) -> None: line = 'help alias > file.txt >> file2.txt' statement = parser.parse(line) assert statement.command == 'help' @@ -431,7 +426,7 @@ def test_redirect_then_append(parser): assert statement.output_to == 'file.txt' -def test_redirect_to_quoted_string(parser): +def test_redirect_to_quoted_string(parser) -> None: line = 'help alias > "file.txt"' statement = parser.parse(line) assert statement.command == 'help' @@ -446,7 +441,7 @@ def test_redirect_to_quoted_string(parser): assert statement.output_to == '"file.txt"' -def test_redirect_to_single_quoted_string(parser): +def test_redirect_to_single_quoted_string(parser) -> None: line = "help alias > 'file.txt'" statement = parser.parse(line) assert statement.command == 'help' @@ -461,7 +456,7 @@ def test_redirect_to_single_quoted_string(parser): assert statement.output_to == "'file.txt'" -def test_redirect_to_empty_quoted_string(parser): +def test_redirect_to_empty_quoted_string(parser) -> None: line = 'help alias > ""' statement = parser.parse(line) assert statement.command == 'help' @@ -476,7 +471,7 @@ def test_redirect_to_empty_quoted_string(parser): assert statement.output_to == '' -def test_redirect_to_empty_single_quoted_string(parser): +def test_redirect_to_empty_single_quoted_string(parser) -> None: line = "help alias > ''" statement = parser.parse(line) assert statement.command == 'help' @@ -491,7 +486,7 @@ def test_redirect_to_empty_single_quoted_string(parser): assert statement.output_to == '' -def test_parse_output_to_paste_buffer(parser): +def test_parse_output_to_paste_buffer(parser) -> None: line = 'output to paste buffer >> ' statement = parser.parse(line) assert statement.command == 'output' @@ -502,10 +497,11 @@ def test_parse_output_to_paste_buffer(parser): assert statement.output == '>>' -def test_parse_redirect_inside_terminator(parser): +def test_parse_redirect_inside_terminator(parser) -> None: """The terminator designates the end of the command/arguments portion. If a redirector occurs before a terminator, then it will be treated as - part of the arguments and not as a redirector.""" + part of the arguments and not as a redirector. + """ line = 'has > inside;' statement = parser.parse(line) assert statement.command == 'has' @@ -517,7 +513,7 @@ def test_parse_redirect_inside_terminator(parser): @pytest.mark.parametrize( - 'line,terminator', + ('line', 'terminator'), [ ('multiline with | inside;', ';'), ('multiline with | inside ;', ';'), @@ -529,7 +525,7 @@ def test_parse_redirect_inside_terminator(parser): ('multiline with | inside &; &;', '&'), ], ) -def test_parse_multiple_terminators(parser, line, terminator): +def test_parse_multiple_terminators(parser, line, terminator) -> None: statement = parser.parse(line) assert statement.multiline_command == 'multiline' assert statement == 'with | inside' @@ -539,7 +535,7 @@ def test_parse_multiple_terminators(parser, line, terminator): assert statement.terminator == terminator -def test_parse_unfinished_multiliine_command(parser): +def test_parse_unfinished_multiliine_command(parser) -> None: line = 'multiline has > inside an unfinished command' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -551,7 +547,7 @@ def test_parse_unfinished_multiliine_command(parser): assert statement.terminator == '' -def test_parse_basic_multiline_command(parser): +def test_parse_basic_multiline_command(parser) -> None: line = 'multiline foo\nbar\n\n' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -565,7 +561,7 @@ def test_parse_basic_multiline_command(parser): @pytest.mark.parametrize( - 'line,terminator', + ('line', 'terminator'), [ ('multiline has > inside;', ';'), ('multiline has > inside;;;', ';'), @@ -574,7 +570,7 @@ def test_parse_basic_multiline_command(parser): ('multiline has > inside & &', '&'), ], ) -def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator): +def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator) -> None: statement = parser.parse(line) assert statement.multiline_command == 'multiline' assert statement == 'has > inside' @@ -584,7 +580,7 @@ def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, ter assert statement.terminator == terminator -def test_parse_multiline_terminated_by_empty_line(parser): +def test_parse_multiline_terminated_by_empty_line(parser) -> None: line = 'multiline command ends\n\n' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -597,7 +593,7 @@ def test_parse_multiline_terminated_by_empty_line(parser): @pytest.mark.parametrize( - 'line,terminator', + ('line', 'terminator'), [ ('multiline command "with\nembedded newline";', ';'), ('multiline command "with\nembedded newline";;;', ';'), @@ -607,7 +603,7 @@ def test_parse_multiline_terminated_by_empty_line(parser): ('multiline command "with\nembedded newline"\n\n', '\n'), ], ) -def test_parse_multiline_with_embedded_newline(parser, line, terminator): +def test_parse_multiline_with_embedded_newline(parser, line, terminator) -> None: statement = parser.parse(line) assert statement.multiline_command == 'multiline' assert statement.command == 'multiline' @@ -618,7 +614,7 @@ def test_parse_multiline_with_embedded_newline(parser, line, terminator): assert statement.terminator == terminator -def test_parse_multiline_ignores_terminators_in_quotes(parser): +def test_parse_multiline_ignores_terminators_in_quotes(parser) -> None: line = 'multiline command "with term; ends" now\n\n' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -630,7 +626,7 @@ def test_parse_multiline_ignores_terminators_in_quotes(parser): assert statement.terminator == '\n' -def test_parse_command_with_unicode_args(parser): +def test_parse_command_with_unicode_args(parser) -> None: line = 'drink café' statement = parser.parse(line) assert statement.command == 'drink' @@ -640,7 +636,7 @@ def test_parse_command_with_unicode_args(parser): assert statement.arg_list == statement.argv[1:] -def test_parse_unicode_command(parser): +def test_parse_unicode_command(parser) -> None: line = 'café au lait' statement = parser.parse(line) assert statement.command == 'café' @@ -650,7 +646,7 @@ def test_parse_unicode_command(parser): assert statement.arg_list == statement.argv[1:] -def test_parse_redirect_to_unicode_filename(parser): +def test_parse_redirect_to_unicode_filename(parser) -> None: line = 'dir home > café' statement = parser.parse(line) assert statement.command == 'dir' @@ -662,12 +658,12 @@ def test_parse_redirect_to_unicode_filename(parser): assert statement.output_to == 'café' -def test_parse_unclosed_quotes(parser): +def test_parse_unclosed_quotes(parser) -> None: with pytest.raises(exceptions.Cmd2ShlexError): _ = parser.tokenize("command with 'unclosed quotes") -def test_empty_statement_raises_exception(): +def test_empty_statement_raises_exception() -> None: app = cmd2.Cmd() with pytest.raises(exceptions.EmptyStatement): app._complete_statement('') @@ -677,7 +673,7 @@ def test_empty_statement_raises_exception(): @pytest.mark.parametrize( - 'line,command,args', + ('line', 'command', 'args'), [ ('helpalias', 'help', ''), ('helpalias mycommand', 'help', 'mycommand'), @@ -688,14 +684,14 @@ def test_empty_statement_raises_exception(): ('l', 'shell', 'ls -al'), ], ) -def test_parse_alias_and_shortcut_expansion(parser, line, command, args): +def test_parse_alias_and_shortcut_expansion(parser, line, command, args) -> None: statement = parser.parse(line) assert statement.command == command assert statement == args assert statement.args == statement -def test_parse_alias_on_multiline_command(parser): +def test_parse_alias_on_multiline_command(parser) -> None: line = 'anothermultiline has > inside an unfinished command' statement = parser.parse(line) assert statement.multiline_command == 'multiline' @@ -706,7 +702,7 @@ def test_parse_alias_on_multiline_command(parser): @pytest.mark.parametrize( - 'line,output', + ('line', 'output'), [ ('helpalias > out.txt', '>'), ('helpalias>out.txt', '>'), @@ -714,7 +710,7 @@ def test_parse_alias_on_multiline_command(parser): ('helpalias>>out.txt', '>>'), ], ) -def test_parse_alias_redirection(parser, line, output): +def test_parse_alias_redirection(parser, line, output) -> None: statement = parser.parse(line) assert statement.command == 'help' assert statement == '' @@ -730,7 +726,7 @@ def test_parse_alias_redirection(parser, line, output): 'helpalias|less', ], ) -def test_parse_alias_pipe(parser, line): +def test_parse_alias_pipe(parser, line) -> None: statement = parser.parse(line) assert statement.command == 'help' assert statement == '' @@ -749,7 +745,7 @@ def test_parse_alias_pipe(parser, line): 'helpalias ;; ;', ], ) -def test_parse_alias_terminator_no_whitespace(parser, line): +def test_parse_alias_terminator_no_whitespace(parser, line) -> None: statement = parser.parse(line) assert statement.command == 'help' assert statement == '' @@ -757,7 +753,7 @@ def test_parse_alias_terminator_no_whitespace(parser, line): assert statement.terminator == ';' -def test_parse_command_only_command_and_args(parser): +def test_parse_command_only_command_and_args(parser) -> None: line = 'help history' statement = parser.parse_command_only(line) assert statement == 'history' @@ -774,7 +770,7 @@ def test_parse_command_only_command_and_args(parser): assert statement.output_to == '' -def test_parse_command_only_strips_line(parser): +def test_parse_command_only_strips_line(parser) -> None: line = ' help history ' statement = parser.parse_command_only(line) assert statement == 'history' @@ -791,7 +787,7 @@ def test_parse_command_only_strips_line(parser): assert statement.output_to == '' -def test_parse_command_only_expands_alias(parser): +def test_parse_command_only_expands_alias(parser) -> None: line = 'fake foobar.py "somebody.py' statement = parser.parse_command_only(line) assert statement == 'foobar.py "somebody.py' @@ -808,7 +804,7 @@ def test_parse_command_only_expands_alias(parser): assert statement.output_to == '' -def test_parse_command_only_expands_shortcuts(parser): +def test_parse_command_only_expands_shortcuts(parser) -> None: line = '!cat foobar.txt' statement = parser.parse_command_only(line) assert statement == 'cat foobar.txt' @@ -826,7 +822,7 @@ def test_parse_command_only_expands_shortcuts(parser): assert statement.output_to == '' -def test_parse_command_only_quoted_args(parser): +def test_parse_command_only_quoted_args(parser) -> None: line = 'l "/tmp/directory with spaces/doit.sh"' statement = parser.parse_command_only(line) assert statement == 'ls -al "/tmp/directory with spaces/doit.sh"' @@ -844,7 +840,7 @@ def test_parse_command_only_quoted_args(parser): assert statement.output_to == '' -def test_parse_command_only_unclosed_quote(parser): +def test_parse_command_only_unclosed_quote(parser) -> None: # Quoted trailing spaces will be preserved line = 'command with unclosed "quote ' statement = parser.parse_command_only(line) @@ -864,7 +860,7 @@ def test_parse_command_only_unclosed_quote(parser): @pytest.mark.parametrize( - 'line,args', + ('line', 'args'), [ ('helpalias > out.txt', '> out.txt'), ('helpalias>out.txt', '>out.txt'), @@ -876,7 +872,7 @@ def test_parse_command_only_unclosed_quote(parser): ('help; ;;', '; ;;'), ], ) -def test_parse_command_only_specialchars(parser, line, args): +def test_parse_command_only_specialchars(parser, line, args) -> None: statement = parser.parse_command_only(line) assert statement == args assert statement.args == args @@ -907,7 +903,7 @@ def test_parse_command_only_specialchars(parser, line, args): '|', ], ) -def test_parse_command_only_empty(parser, line): +def test_parse_command_only_empty(parser, line) -> None: statement = parser.parse_command_only(line) assert statement == '' assert statement.args == statement @@ -924,7 +920,7 @@ def test_parse_command_only_empty(parser, line): assert statement.output_to == '' -def test_parse_command_only_multiline(parser): +def test_parse_command_only_multiline(parser) -> None: line = 'multiline with partially "open quotes and no terminator' statement = parser.parse_command_only(line) assert statement.command == 'multiline' @@ -934,7 +930,7 @@ def test_parse_command_only_multiline(parser): assert statement.args == statement -def test_statement_initialization(): +def test_statement_initialization() -> None: string = 'alias' statement = cmd2.Statement(string) assert string == statement @@ -954,7 +950,7 @@ def test_statement_initialization(): assert statement.output_to == '' -def test_statement_is_immutable(): +def test_statement_is_immutable() -> None: string = 'foo' statement = cmd2.Statement(string) assert string == statement @@ -966,7 +962,7 @@ def test_statement_is_immutable(): statement.raw = 'baz' -def test_statement_as_dict(parser): +def test_statement_as_dict(parser) -> None: # Make sure to_dict() results can be restored to identical Statement statement = parser.parse("!ls > out.txt") assert statement == Statement.from_dict(statement.to_dict()) @@ -986,45 +982,54 @@ def test_statement_as_dict(parser): Statement.from_dict(statement_dict) -def test_is_valid_command_invalid(mocker, parser): +def test_is_valid_command_invalid(mocker, parser) -> None: # Non-string command valid, errmsg = parser.is_valid_command(5) - assert not valid and 'must be a string' in errmsg + assert not valid + assert 'must be a string' in errmsg mock = mocker.MagicMock() valid, errmsg = parser.is_valid_command(mock) - assert not valid and 'must be a string' in errmsg + assert not valid + assert 'must be a string' in errmsg # Empty command valid, errmsg = parser.is_valid_command('') - assert not valid and 'cannot be an empty string' in errmsg + assert not valid + assert 'cannot be an empty string' in errmsg # Start with the comment character valid, errmsg = parser.is_valid_command(constants.COMMENT_CHAR) - assert not valid and 'cannot start with the comment character' in errmsg + assert not valid + assert 'cannot start with the comment character' in errmsg # Starts with shortcut valid, errmsg = parser.is_valid_command('!ls') - assert not valid and 'cannot start with a shortcut' in errmsg + assert not valid + assert 'cannot start with a shortcut' in errmsg # Contains whitespace valid, errmsg = parser.is_valid_command('shell ls') - assert not valid and 'cannot contain: whitespace, quotes,' in errmsg + assert not valid + assert 'cannot contain: whitespace, quotes,' in errmsg # Contains a quote valid, errmsg = parser.is_valid_command('"shell"') - assert not valid and 'cannot contain: whitespace, quotes,' in errmsg + assert not valid + assert 'cannot contain: whitespace, quotes,' in errmsg # Contains a redirector valid, errmsg = parser.is_valid_command('>shell') - assert not valid and 'cannot contain: whitespace, quotes,' in errmsg + assert not valid + assert 'cannot contain: whitespace, quotes,' in errmsg # Contains a terminator valid, errmsg = parser.is_valid_command(';shell') - assert not valid and 'cannot contain: whitespace, quotes,' in errmsg + assert not valid + assert 'cannot contain: whitespace, quotes,' in errmsg -def test_is_valid_command_valid(parser): +def test_is_valid_command_valid(parser) -> None: # Valid command valid, errmsg = parser.is_valid_command('shell') assert valid @@ -1036,7 +1041,7 @@ def test_is_valid_command_valid(parser): assert not errmsg -def test_macro_normal_arg_pattern(): +def test_macro_normal_arg_pattern() -> None: # 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, @@ -1090,7 +1095,7 @@ def test_macro_normal_arg_pattern(): assert not matches -def test_macro_escaped_arg_pattern(): +def test_macro_escaped_arg_pattern() -> None: # This pattern matches digits surrounded by 2 or more braces on both sides from cmd2.parsing import ( MacroArg, diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 5a625b724..56c2f2d56 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,11 +1,8 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Test plugin infrastructure and hooks. -""" +"""Test plugin infrastructure and hooks.""" import argparse import sys +from typing import NoReturn from unittest import ( mock, ) @@ -24,11 +21,11 @@ class Plugin: """A mixin class for testing hook registration and calling""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.reset_counters() - def reset_counters(self): + def reset_counters(self) -> None: self.called_preparse = 0 self.called_postparsing = 0 self.called_precmd = 0 @@ -51,11 +48,9 @@ def prepost_hook_two(self) -> None: def prepost_hook_too_many_parameters(self, param) -> None: """A preloop or postloop hook with too many parameters""" - pass def prepost_hook_with_wrong_return_annotation(self) -> bool: """A preloop or postloop hook with incorrect return type""" - pass ### # @@ -95,23 +90,18 @@ def postparse_hook_exception(self, data: cmd2.plugin.PostparsingData) -> cmd2.pl def postparse_hook_too_many_parameters(self, data1, data2) -> cmd2.plugin.PostparsingData: """A postparsing hook with too many parameters""" - pass def postparse_hook_undeclared_parameter_annotation(self, data) -> cmd2.plugin.PostparsingData: """A postparsing hook with an undeclared parameter type""" - pass def postparse_hook_wrong_parameter_annotation(self, data: str) -> cmd2.plugin.PostparsingData: """A postparsing hook with the wrong parameter type""" - pass - def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData): + def postparse_hook_undeclared_return_annotation(self, data: cmd2.plugin.PostparsingData) -> None: """A postparsing hook with an undeclared return type""" - pass def postparse_hook_wrong_return_annotation(self, data: cmd2.plugin.PostparsingData) -> str: """A postparsing hook with the wrong return type""" - pass ### # @@ -140,7 +130,6 @@ def precmd_hook_exception(self, data: plugin.PrecommandData) -> plugin.Precomman def precmd_hook_not_enough_parameters(self) -> plugin.PrecommandData: """A precommand hook with no parameters""" - pass def precmd_hook_too_many_parameters(self, one: plugin.PrecommandData, two: str) -> plugin.PrecommandData: """A precommand hook with too many parameters""" @@ -183,7 +172,6 @@ def postcmd_hook_exception(self, data: plugin.PostcommandData) -> plugin.Postcom def postcmd_hook_not_enough_parameters(self) -> plugin.PostcommandData: """A precommand hook with no parameters""" - pass def postcmd_hook_too_many_parameters(self, one: plugin.PostcommandData, two: str) -> plugin.PostcommandData: """A precommand hook with too many parameters""" @@ -249,7 +237,6 @@ def cmdfinalization_hook_passthrough_exception( def cmdfinalization_hook_not_enough_parameters(self) -> plugin.CommandFinalizationData: """A command finalization hook with no parameters.""" - pass def cmdfinalization_hook_too_many_parameters( self, one: plugin.CommandFinalizationData, two: str @@ -277,14 +264,14 @@ def cmdfinalization_hook_wrong_return_annotation(self, data: plugin.CommandFinal class PluggedApp(Plugin, cmd2.Cmd): """A sample app with a plugin mixed in""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - def do_say(self, statement): + def do_say(self, statement) -> None: """Repeat back the arguments""" self.poutput(statement) - def do_skip_postcmd_hooks(self, _): + def do_skip_postcmd_hooks(self, _) -> NoReturn: self.poutput("In do_skip_postcmd_hooks") raise exceptions.SkipPostcommandHooks @@ -292,7 +279,7 @@ def do_skip_postcmd_hooks(self, _): parser.add_argument("my_arg", help="some help text") @with_argparser(parser) - def do_argparse_cmd(self, namespace: argparse.Namespace): + def do_argparse_cmd(self, namespace: argparse.Namespace) -> None: """Repeat back the arguments""" self.poutput(namespace.cmd2_statement.get()) @@ -302,19 +289,19 @@ def do_argparse_cmd(self, namespace: argparse.Namespace): # test pre and postloop hooks # ### -def test_register_preloop_hook_too_many_parameters(): +def test_register_preloop_hook_too_many_parameters() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_too_many_parameters) -def test_register_preloop_hook_with_return_annotation(): +def test_register_preloop_hook_with_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_preloop_hook(app.prepost_hook_with_wrong_return_annotation) -def test_preloop_hook(capsys): +def test_preloop_hook(capsys) -> None: # 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'] @@ -328,7 +315,7 @@ def test_preloop_hook(capsys): assert not err -def test_preloop_hooks(capsys): +def test_preloop_hooks(capsys) -> None: # 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'] @@ -343,19 +330,19 @@ def test_preloop_hooks(capsys): assert not err -def test_register_postloop_hook_too_many_parameters(): +def test_register_postloop_hook_too_many_parameters() -> None: 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(): +def test_register_postloop_hook_with_wrong_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postloop_hook(app.prepost_hook_with_wrong_return_annotation) -def test_postloop_hook(capsys): +def test_postloop_hook(capsys) -> None: # 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'] @@ -369,7 +356,7 @@ def test_postloop_hook(capsys): assert not err -def test_postloop_hooks(capsys): +def test_postloop_hooks(capsys) -> None: # 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'] @@ -389,7 +376,7 @@ def test_postloop_hooks(capsys): # test preparse hook # ### -def test_preparse(capsys): +def test_preparse(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.preparse) app.onecmd_plus_hooks('say hello') @@ -404,37 +391,37 @@ def test_preparse(capsys): # test postparsing hooks # ### -def test_postparsing_hook_too_many_parameters(): +def test_postparsing_hook_too_many_parameters() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_too_many_parameters) -def test_postparsing_hook_undeclared_parameter_annotation(): +def test_postparsing_hook_undeclared_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_parameter_annotation) -def test_postparsing_hook_wrong_parameter_annotation(): +def test_postparsing_hook_wrong_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_parameter_annotation) -def test_postparsing_hook_undeclared_return_annotation(): +def test_postparsing_hook_undeclared_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_undeclared_return_annotation) -def test_postparsing_hook_wrong_return_annotation(): +def test_postparsing_hook_wrong_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postparsing_hook(app.postparse_hook_wrong_return_annotation) -def test_postparsing_hook(capsys): +def test_postparsing_hook(capsys) -> None: app = PluggedApp() app.onecmd_plus_hooks('say hello') out, err = capsys.readouterr() @@ -460,7 +447,7 @@ def test_postparsing_hook(capsys): assert app.called_postparsing == 2 -def test_postparsing_hook_stop_first(capsys): +def test_postparsing_hook_stop_first(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_stop) stop = app.onecmd_plus_hooks('say hello') @@ -475,7 +462,7 @@ def test_postparsing_hook_stop_first(capsys): assert stop -def test_postparsing_hook_stop_second(capsys): +def test_postparsing_hook_stop_second(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) stop = app.onecmd_plus_hooks('say hello') @@ -497,7 +484,7 @@ def test_postparsing_hook_stop_second(capsys): assert stop -def test_postparsing_hook_emptystatement_first(capsys): +def test_postparsing_hook_emptystatement_first(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_emptystatement) stop = app.onecmd_plus_hooks('say hello') @@ -518,7 +505,7 @@ def test_postparsing_hook_emptystatement_first(capsys): assert app.called_postparsing == 1 -def test_postparsing_hook_emptystatement_second(capsys): +def test_postparsing_hook_emptystatement_second(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.postparse_hook) stop = app.onecmd_plus_hooks('say hello') @@ -549,7 +536,7 @@ def test_postparsing_hook_emptystatement_second(capsys): assert app.called_postparsing == 2 -def test_postparsing_hook_exception(capsys): +def test_postparsing_hook_exception(capsys) -> None: app = PluggedApp() app.register_postparsing_hook(app.postparse_hook_exception) stop = app.onecmd_plus_hooks('say hello') @@ -575,7 +562,7 @@ def test_postparsing_hook_exception(capsys): # test precmd hooks # ##### -def test_register_precmd_hook_parameter_count(): +def test_register_precmd_hook_parameter_count() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_not_enough_parameters) @@ -583,31 +570,31 @@ def test_register_precmd_hook_parameter_count(): app.register_precmd_hook(app.precmd_hook_too_many_parameters) -def test_register_precmd_hook_no_parameter_annotation(): +def test_register_precmd_hook_no_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_parameter_annotation) -def test_register_precmd_hook_wrong_parameter_annotation(): +def test_register_precmd_hook_wrong_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_parameter_annotation) -def test_register_precmd_hook_no_return_annotation(): +def test_register_precmd_hook_no_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_no_return_annotation) -def test_register_precmd_hook_wrong_return_annotation(): +def test_register_precmd_hook_wrong_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_precmd_hook(app.precmd_hook_wrong_return_annotation) -def test_precmd_hook(capsys): +def test_precmd_hook(capsys) -> None: app = PluggedApp() app.onecmd_plus_hooks('say hello') out, err = capsys.readouterr() @@ -636,7 +623,7 @@ def test_precmd_hook(capsys): assert app.called_precmd == 3 -def test_precmd_hook_emptystatement_first(capsys): +def test_precmd_hook_emptystatement_first(capsys) -> None: app = PluggedApp() app.register_precmd_hook(app.precmd_hook_emptystatement) stop = app.onecmd_plus_hooks('say hello') @@ -662,7 +649,7 @@ def test_precmd_hook_emptystatement_first(capsys): assert app.called_precmd == 1 -def test_precmd_hook_emptystatement_second(capsys): +def test_precmd_hook_emptystatement_second(capsys) -> None: app = PluggedApp() app.register_precmd_hook(app.precmd_hook) stop = app.onecmd_plus_hooks('say hello') @@ -704,7 +691,7 @@ def test_precmd_hook_emptystatement_second(capsys): # test postcmd hooks # #### -def test_register_postcmd_hook_parameter_count(): +def test_register_postcmd_hook_parameter_count() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_not_enough_parameters) @@ -712,31 +699,31 @@ def test_register_postcmd_hook_parameter_count(): app.register_postcmd_hook(app.postcmd_hook_too_many_parameters) -def test_register_postcmd_hook_no_parameter_annotation(): +def test_register_postcmd_hook_no_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_parameter_annotation) -def test_register_postcmd_hook_wrong_parameter_annotation(): +def test_register_postcmd_hook_wrong_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_parameter_annotation) -def test_register_postcmd_hook_no_return_annotation(): +def test_register_postcmd_hook_no_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_no_return_annotation) -def test_register_postcmd_hook_wrong_return_annotation(): +def test_register_postcmd_hook_wrong_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_postcmd_hook(app.postcmd_hook_wrong_return_annotation) -def test_postcmd(capsys): +def test_postcmd(capsys) -> None: app = PluggedApp() app.onecmd_plus_hooks('say hello') out, err = capsys.readouterr() @@ -765,7 +752,7 @@ def test_postcmd(capsys): assert app.called_postcmd == 3 -def test_postcmd_exception_first(capsys): +def test_postcmd_exception_first(capsys) -> None: app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook_exception) stop = app.onecmd_plus_hooks('say hello') @@ -792,7 +779,7 @@ def test_postcmd_exception_first(capsys): assert app.called_postcmd == 1 -def test_postcmd_exception_second(capsys): +def test_postcmd_exception_second(capsys) -> None: app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) stop = app.onecmd_plus_hooks('say hello') @@ -823,7 +810,7 @@ def test_postcmd_exception_second(capsys): # command finalization # ### -def test_register_cmdfinalization_hook_parameter_count(): +def test_register_cmdfinalization_hook_parameter_count() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_not_enough_parameters) @@ -831,31 +818,31 @@ def test_register_cmdfinalization_hook_parameter_count(): app.register_cmdfinalization_hook(app.cmdfinalization_hook_too_many_parameters) -def test_register_cmdfinalization_hook_no_parameter_annotation(): +def test_register_cmdfinalization_hook_no_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_parameter_annotation) -def test_register_cmdfinalization_hook_wrong_parameter_annotation(): +def test_register_cmdfinalization_hook_wrong_parameter_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_parameter_annotation) -def test_register_cmdfinalization_hook_no_return_annotation(): +def test_register_cmdfinalization_hook_no_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_no_return_annotation) -def test_register_cmdfinalization_hook_wrong_return_annotation(): +def test_register_cmdfinalization_hook_wrong_return_annotation() -> None: app = PluggedApp() with pytest.raises(TypeError): app.register_cmdfinalization_hook(app.cmdfinalization_hook_wrong_return_annotation) -def test_cmdfinalization(capsys): +def test_cmdfinalization(capsys) -> None: app = PluggedApp() app.onecmd_plus_hooks('say hello') out, err = capsys.readouterr() @@ -880,7 +867,7 @@ def test_cmdfinalization(capsys): assert app.called_cmdfinalization == 2 -def test_cmdfinalization_stop_first(capsys): +def test_cmdfinalization_stop_first(capsys) -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop) app.register_cmdfinalization_hook(app.cmdfinalization_hook) @@ -892,7 +879,7 @@ def test_cmdfinalization_stop_first(capsys): assert stop -def test_cmdfinalization_stop_second(capsys): +def test_cmdfinalization_stop_second(capsys) -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook) app.register_cmdfinalization_hook(app.cmdfinalization_hook_stop) @@ -904,7 +891,7 @@ def test_cmdfinalization_stop_second(capsys): assert stop -def test_cmdfinalization_hook_exception(capsys): +def test_cmdfinalization_hook_exception(capsys) -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_exception) stop = app.onecmd_plus_hooks('say hello') @@ -925,7 +912,7 @@ def test_cmdfinalization_hook_exception(capsys): assert app.called_cmdfinalization == 1 -def test_cmdfinalization_hook_system_exit(): +def test_cmdfinalization_hook_system_exit() -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit) stop = app.onecmd_plus_hooks('say hello') @@ -934,7 +921,7 @@ def test_cmdfinalization_hook_system_exit(): assert app.exit_code == 5 -def test_cmdfinalization_hook_keyboard_interrupt(): +def test_cmdfinalization_hook_keyboard_interrupt() -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt) @@ -957,17 +944,17 @@ def test_cmdfinalization_hook_keyboard_interrupt(): assert app.called_cmdfinalization == 1 -def test_cmdfinalization_hook_passthrough_exception(): +def test_cmdfinalization_hook_passthrough_exception() -> None: app = PluggedApp() app.register_cmdfinalization_hook(app.cmdfinalization_hook_passthrough_exception) - with pytest.raises(OSError) as excinfo: + expected_err = "Pass me up" + with pytest.raises(OSError, match=expected_err): app.onecmd_plus_hooks('say hello') - assert 'Pass me up' in str(excinfo.value) assert app.called_cmdfinalization == 1 -def test_skip_postcmd_hooks(capsys): +def test_skip_postcmd_hooks(capsys) -> None: app = PluggedApp() app.register_postcmd_hook(app.postcmd_hook) app.register_cmdfinalization_hook(app.cmdfinalization_hook) @@ -980,9 +967,8 @@ def test_skip_postcmd_hooks(capsys): assert app.called_cmdfinalization == 1 -def test_cmd2_argparse_exception(capsys): - """ - Verify Cmd2ArgparseErrors raised after calling a command prevent postcmd events from +def test_cmd2_argparse_exception(capsys) -> None: + """Verify Cmd2ArgparseErrors raised after calling a command prevent postcmd events from running but do not affect cmdfinalization events """ app = PluggedApp() diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index 594a30b75..a64f77ba9 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit/functional testing for run_pytest in cmd2 -""" +"""Unit/functional testing for run_pytest in cmd2""" import builtins import os @@ -31,84 +27,84 @@ def cmdfinalization_hook(data: plugin.CommandFinalizationData) -> plugin.Command return data -def test_run_pyscript(base_app, request): +def test_run_pyscript(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') expected = 'This is a python script running ...' - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) + out, err = run_cmd(base_app, f"run_pyscript {python_script}") assert expected in out assert base_app.last_result is True -def test_run_pyscript_recursive_not_allowed(base_app, request): +def test_run_pyscript_recursive_not_allowed(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'recursive.py') expected = 'Recursively entering interactive Python shells is not allowed' - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) + out, err = run_cmd(base_app, f"run_pyscript {python_script}") assert err[0] == expected assert base_app.last_result is False -def test_run_pyscript_with_nonexist_file(base_app): +def test_run_pyscript_with_nonexist_file(base_app) -> None: python_script = 'does_not_exist.py' - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) + out, err = run_cmd(base_app, f"run_pyscript {python_script}") assert "Error reading script file" in err[0] assert base_app.last_result is False -def test_run_pyscript_with_non_python_file(base_app, request): +def test_run_pyscript_with_non_python_file(base_app, request) -> None: m = mock.MagicMock(name='input', return_value='2') builtins.input = m test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'help.txt') - out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename)) + out, err = run_cmd(base_app, f'run_pyscript {filename}') assert "does not have a .py extension" in err[0] assert base_app.last_result is False @pytest.mark.parametrize('python_script', odd_file_names) -def test_run_pyscript_with_odd_file_names(base_app, python_script): - """ - Pass in file names with various patterns. Since these files don't exist, we will rely +def test_run_pyscript_with_odd_file_names(base_app, python_script) -> None: + """Pass in file names with various patterns. Since these files don't exist, we will rely on the error text to make sure the file names were processed correctly. """ # Mock input to get us passed the warning about not ending in .py input_mock = mock.MagicMock(name='input', return_value='1') builtins.input = input_mock - out, err = run_cmd(base_app, "run_pyscript {}".format(utils.quote_string(python_script))) + out, err = run_cmd(base_app, f"run_pyscript {utils.quote_string(python_script)}") err = ''.join(err) - assert "Error reading script file '{}'".format(python_script) in err + assert f"Error reading script file '{python_script}'" in err assert base_app.last_result is False -def test_run_pyscript_with_exception(base_app, request): +def test_run_pyscript_with_exception(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'raises_exception.py') - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) + out, err = run_cmd(base_app, f"run_pyscript {python_script}") assert err[0].startswith('Traceback') assert "TypeError: unsupported operand type(s) for +: 'int' and 'str'" in err[-1] assert base_app.last_result is True -def test_run_pyscript_requires_an_argument(base_app): +def test_run_pyscript_requires_an_argument(base_app) -> None: out, err = run_cmd(base_app, "run_pyscript") assert "the following arguments are required: script_path" in err[1] assert base_app.last_result is None -def test_run_pyscript_help(base_app, request): +def test_run_pyscript_help(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'help.py') out1, err1 = run_cmd(base_app, 'help') - out2, err2 = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) - assert out1 and out1 == out2 + out2, err2 = run_cmd(base_app, f'run_pyscript {python_script}') + assert out1 + assert out1 == out2 -def test_scripts_add_to_history(base_app, request): +def test_scripts_add_to_history(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'help.py') command = f'run_pyscript {python_script}' @@ -129,63 +125,63 @@ def test_scripts_add_to_history(base_app, request): assert base_app.history.get(1).raw == command -def test_run_pyscript_dir(base_app, request): +def test_run_pyscript_dir(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'pyscript_dir.py') - out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + out, err = run_cmd(base_app, f'run_pyscript {python_script}') assert out[0] == "['cmd_echo']" -def test_run_pyscript_stdout_capture(base_app, request): +def test_run_pyscript_stdout_capture(base_app, request) -> None: base_app.register_cmdfinalization_hook(cmdfinalization_hook) test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'stdout_capture.py') - out, err = run_cmd(base_app, 'run_pyscript {} {}'.format(python_script, HOOK_OUTPUT)) + out, err = run_cmd(base_app, f'run_pyscript {python_script} {HOOK_OUTPUT}') assert out[0] == "PASSED" assert out[1] == "PASSED" -def test_run_pyscript_stop(base_app, request): +def test_run_pyscript_stop(base_app, request) -> None: # Verify onecmd_plus_hooks() returns True if any commands in a pyscript return True for stop test_dir = os.path.dirname(request.module.__file__) # help.py doesn't run any commands that return True for stop python_script = os.path.join(test_dir, 'pyscript', 'help.py') - stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script)) + stop = base_app.onecmd_plus_hooks(f'run_pyscript {python_script}') assert not stop # stop.py runs the quit command which does return True for stop python_script = os.path.join(test_dir, 'pyscript', 'stop.py') - stop = base_app.onecmd_plus_hooks('run_pyscript {}'.format(python_script)) + stop = base_app.onecmd_plus_hooks(f'run_pyscript {python_script}') assert stop -def test_run_pyscript_environment(base_app, request): +def test_run_pyscript_environment(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'environment.py') - out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + out, err = run_cmd(base_app, f'run_pyscript {python_script}') assert out[0] == "PASSED" -def test_run_pyscript_self_in_py(base_app, request): +def test_run_pyscript_self_in_py(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'self_in_py.py') # Set self_in_py to True and make sure we see self base_app.self_in_py = True - out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + out, err = run_cmd(base_app, f'run_pyscript {python_script}') assert 'I see self' in out[0] # Set self_in_py to False and make sure we can't see self base_app.self_in_py = False - out, err = run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + out, err = run_cmd(base_app, f'run_pyscript {python_script}') assert 'I do not see self' in out[0] -def test_run_pyscript_py_locals(base_app, request): +def test_run_pyscript_py_locals(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'pyscript', 'py_locals.py') @@ -197,7 +193,7 @@ def test_run_pyscript_py_locals(base_app, request): # this object should be editable from the py environment. base_app.py_locals['my_list'] = [] - run_cmd(base_app, 'run_pyscript {}'.format(python_script)) + run_cmd(base_app, f'run_pyscript {python_script}') # test_var should still exist assert base_app.py_locals['test_var'] == 5 @@ -206,10 +202,10 @@ def test_run_pyscript_py_locals(base_app, request): assert base_app.py_locals['my_list'][0] == 2 -def test_run_pyscript_app_echo(base_app, request): +def test_run_pyscript_app_echo(base_app, request) -> None: 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)) + out, err = run_cmd(base_app, f'run_pyscript {python_script}') # Only the edit help text should have been echoed to pytest's stdout assert out[0] == "Usage: edit [-h] [file_path]" diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index fbbdfbc4a..caf19b7eb 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E501 -""" -Unit testing for cmd2/table_creator.py module -""" +"""Unit testing for cmd2/table_creator.py module""" import pytest @@ -27,20 +23,18 @@ # fmt: off -def test_column_creation(): +def test_column_creation() -> None: # Width less than 1 - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Column width cannot be less than 1"): Column("Column 1", width=0) - assert "Column width cannot be less than 1" in str(excinfo.value) # Width specified c = Column("header", width=20) assert c.width == 20 # max_data_lines less than 1 - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Max data lines cannot be less than 1"): Column("Column 1", max_data_lines=0) - assert "Max data lines cannot be less than 1" in str(excinfo.value) # No width specified, blank label c = Column("") @@ -86,7 +80,7 @@ def test_column_creation(): assert c.style_data_text is False -def test_column_alignment(): +def test_column_alignment() -> None: column_1 = Column( "Col 1", width=10, @@ -141,7 +135,7 @@ def test_column_alignment(): ) -def test_blank_last_line(): +def test_blank_last_line() -> None: """This tests that an empty line is inserted when the last data line is blank""" column_1 = Column("Col 1", width=10) tc = TableCreator([column_1]) @@ -160,7 +154,7 @@ def test_blank_last_line(): assert row == ' ' -def test_wrap_text(): +def test_wrap_text() -> None: column_1 = Column("Col 1", width=10) tc = TableCreator([column_1]) @@ -182,7 +176,7 @@ def test_wrap_text(): ' last one ') -def test_wrap_text_max_lines(): +def test_wrap_text_max_lines() -> None: column_1 = Column("Col 1", width=10, max_data_lines=2) tc = TableCreator([column_1]) @@ -217,7 +211,7 @@ def test_wrap_text_max_lines(): 'last line…') -def test_wrap_long_word(): +def test_wrap_long_word() -> None: # Make sure words wider than column start on own line and wrap column_1 = Column("LongColumnName", width=10) column_2 = Column("Col 2", width=10) @@ -232,7 +226,7 @@ def test_wrap_long_word(): 'Name Col 2 ') # Test data row - row_data = list() + row_data = [] # Long word should start on the first line (style should not affect width) row_data.append(ansi.style("LongerThan10", fg=Fg.GREEN)) @@ -260,7 +254,7 @@ def test_wrap_long_word(): assert row == expected -def test_wrap_long_word_max_data_lines(): +def test_wrap_long_word_max_data_lines() -> None: column_1 = Column("Col 1", width=10, max_data_lines=2) column_2 = Column("Col 2", width=10, max_data_lines=2) column_3 = Column("Col 3", width=10, max_data_lines=2) @@ -269,7 +263,7 @@ def test_wrap_long_word_max_data_lines(): columns = [column_1, column_2, column_3, column_4] tc = TableCreator(columns) - row_data = list() + row_data = [] # This long word will exactly fit the last line and it's the final word in the text. No ellipsis should appear. row_data.append("LongerThan10FitsLast") @@ -289,9 +283,8 @@ def test_wrap_long_word_max_data_lines(): '10FitsLast 10FitsLas… 10RunsOve… ') -def test_wrap_long_char_wider_than_max_width(): - """ - This tests case where a character is wider than max_width. This can happen if max_width +def test_wrap_long_char_wider_than_max_width() -> None: + """This tests case where a character is wider than max_width. This can happen if max_width is 1 and the text contains wide characters (e.g. East Asian). Replace it with an ellipsis. """ column_1 = Column("Col 1", width=1) @@ -300,7 +293,7 @@ def test_wrap_long_char_wider_than_max_width(): assert row == '…' -def test_generate_row_exceptions(): +def test_generate_row_exceptions() -> None: column_1 = Column("Col 1") tc = TableCreator([column_1]) row_data = ['fake'] @@ -313,18 +306,16 @@ def test_generate_row_exceptions(): # Unprintable characters for arg in ['fill_char', 'pre_line', 'inter_cell', 'post_line']: kwargs = {arg: '\n'} - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match=f"{arg} contains an unprintable character"): tc.generate_row(row_data=row_data, is_header=False, **kwargs) - assert "{} contains an unprintable character".format(arg) in str(excinfo.value) # Data with too many columns row_data = ['Data 1', 'Extra Column'] - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Length of row_data must match length of cols"): tc.generate_row(row_data=row_data, is_header=False) - assert "Length of row_data must match length of cols" in str(excinfo.value) -def test_tabs(): +def test_tabs() -> None: column_1 = Column("Col\t1", width=20) column_2 = Column("Col 2") columns = [column_1, column_2] @@ -334,16 +325,15 @@ def test_tabs(): row = tc.generate_row(row_data, is_header=True, fill_char='\t', pre_line='\t', inter_cell='\t', post_line='\t') assert row == ' Col 1 Col 2 ' - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Tab width cannot be less than 1" ): TableCreator([column_1, column_2], tab_width=0) - assert "Tab width cannot be less than 1" in str(excinfo.value) -def test_simple_table_creation(): +def test_simple_table_creation() -> None: column_1 = Column("Col 1", width=16) column_2 = Column("Col 2", width=16) - row_data = list() + row_data = [] row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) @@ -441,24 +431,20 @@ def test_simple_table_creation(): ) # Invalid column spacing - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Column spacing cannot be less than 0"): SimpleTable([column_1, column_2], column_spacing=-1) - assert "Column spacing cannot be less than 0" in str(excinfo.value) # Invalid divider character - with pytest.raises(TypeError) as excinfo: + with pytest.raises(TypeError, match="Divider character must be exactly one character long"): SimpleTable([column_1, column_2], divider_char='too long') - assert "Divider character must be exactly one character long" in str(excinfo.value) - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Divider character is an unprintable character"): SimpleTable([column_1, column_2], divider_char='\n') - assert "Divider character is an unprintable character" in str(excinfo.value) # Invalid row spacing st = SimpleTable([column_1, column_2]) - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Row spacing cannot be less than 0"): st.generate_table(row_data, row_spacing=-1) - assert "Row spacing cannot be less than 0" in str(excinfo.value) # Test header and data colors st = SimpleTable([column_1, column_2], divider_char=None, header_bg=Bg.GREEN, data_bg=Bg.LIGHT_BLUE) @@ -483,21 +469,20 @@ def test_simple_table_creation(): ) -def test_simple_table_width(): +def test_simple_table_width() -> None: # Base width for num_cols in range(1, 10): assert SimpleTable.base_width(num_cols) == (num_cols - 1) * 2 # Invalid num_cols value - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Column count cannot be less than 1"): SimpleTable.base_width(0) - assert "Column count cannot be less than 1" in str(excinfo.value) # Total width column_1 = Column("Col 1", width=16) column_2 = Column("Col 2", width=16) - row_data = list() + row_data = [] row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) @@ -505,22 +490,21 @@ def test_simple_table_width(): assert st.total_width() == 34 -def test_simple_generate_data_row_exceptions(): +def test_simple_generate_data_row_exceptions() -> None: column_1 = Column("Col 1") tc = SimpleTable([column_1]) # Data with too many columns row_data = ['Data 1', 'Extra Column'] - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Length of row_data must match length of cols"): tc.generate_data_row(row_data=row_data) - assert "Length of row_data must match length of cols" in str(excinfo.value) -def test_bordered_table_creation(): +def test_bordered_table_creation() -> None: column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) - row_data = list() + row_data = [] row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) @@ -575,9 +559,8 @@ def test_bordered_table_creation(): ) # Invalid padding - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Padding cannot be less than 0"): BorderedTable([column_1, column_2], padding=-1) - assert "Padding cannot be less than 0" in str(excinfo.value) # Test border, header, and data colors bt = BorderedTable([column_1, column_2], border_fg=Fg.LIGHT_YELLOW, border_bg=Bg.WHITE, @@ -609,7 +592,7 @@ def test_bordered_table_creation(): ) -def test_bordered_table_width(): +def test_bordered_table_width() -> None: # Default behavior (column_borders=True, padding=1) assert BorderedTable.base_width(1) == 4 assert BorderedTable.base_width(2) == 7 @@ -631,15 +614,14 @@ def test_bordered_table_width(): assert BorderedTable.base_width(3, padding=3) == 22 # Invalid num_cols value - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Column count cannot be less than 1"): BorderedTable.base_width(0) - assert "Column count cannot be less than 1" in str(excinfo.value) # Total width column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) - row_data = list() + row_data = [] row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) @@ -647,22 +629,21 @@ def test_bordered_table_width(): assert bt.total_width() == 37 -def test_bordered_generate_data_row_exceptions(): +def test_bordered_generate_data_row_exceptions() -> None: column_1 = Column("Col 1") tc = BorderedTable([column_1]) # Data with too many columns row_data = ['Data 1', 'Extra Column'] - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Length of row_data must match length of cols"): tc.generate_data_row(row_data=row_data) - assert "Length of row_data must match length of cols" in str(excinfo.value) -def test_alternating_table_creation(): +def test_alternating_table_creation() -> None: column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) - row_data = list() + row_data = [] row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) @@ -713,9 +694,8 @@ def test_alternating_table_creation(): ) # Invalid padding - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError, match="Padding cannot be less than 0"): AlternatingTable([column_1, column_2], padding=-1) - assert "Padding cannot be less than 0" in str(excinfo.value) # Test border, header, and data colors at = AlternatingTable([column_1, column_2], border_fg=Fg.LIGHT_YELLOW, border_bg=Bg.WHITE, diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 986221ff2..c94f2b8c1 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -1,14 +1,11 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Cmd2 functional testing based on transcript -""" +"""Cmd2 functional testing based on transcript""" import os import random import re import sys import tempfile +from typing import NoReturn from unittest import ( mock, ) @@ -35,7 +32,7 @@ class CmdLineApp(cmd2.Cmd): MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: self.maxrepeats = 3 super().__init__(*args, multiline_commands=['orate'], **kwargs) @@ -51,11 +48,11 @@ def __init__(self, *args, **kwargs): speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") @cmd2.with_argparser(speak_parser, with_unknown_args=True) - def do_speak(self, opts, arg): + def do_speak(self, opts, arg) -> None: """Repeats what you tell me to.""" arg = ' '.join(arg) if opts.piglatin: - arg = '%s%say' % (arg[1:], arg[0]) + arg = f'{arg[1:]}{arg[0]}ay' if opts.shout: arg = arg.upper() repetitions = opts.repeat or 1 @@ -72,10 +69,9 @@ def do_speak(self, opts, arg): mumble_parser.add_argument('-r', '--repeat', type=int, help="output [n] times") @cmd2.with_argparser(mumble_parser, with_unknown_args=True) - def do_mumble(self, opts, arg): + def do_mumble(self, opts, arg) -> None: """Mumbles what you tell me to.""" repetitions = opts.repeat or 1 - # arg = arg.split() for _ in range(min(repetitions, self.maxrepeats)): output = [] if random.random() < 0.33: @@ -88,15 +84,14 @@ def do_mumble(self, opts, arg): output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) - def do_nothing(self, statement): + def do_nothing(self, statement) -> None: """Do nothing and output nothing""" - pass - def do_keyboard_interrupt(self, _): + def do_keyboard_interrupt(self, _) -> NoReturn: raise KeyboardInterrupt('Interrupting this command') -def test_commands_at_invocation(): +def test_commands_at_invocation() -> None: testargs = ["prog", "say hello", "say Gracie", "quit"] expected = "This is an intro banner ...\nhello\nGracie\n" with mock.patch.object(sys, 'argv', testargs): @@ -109,7 +104,7 @@ def test_commands_at_invocation(): @pytest.mark.parametrize( - 'filename,feedback_to_output', + ('filename', 'feedback_to_output'), [ ('bol_eol.txt', False), ('characterclass.txt', False), @@ -128,7 +123,7 @@ def test_commands_at_invocation(): ('word_boundaries.txt', False), ], ) -def test_transcript(request, capsys, filename, feedback_to_output): +def test_transcript(request, capsys, filename, feedback_to_output) -> None: # Get location of the transcript test_dir = os.path.dirname(request.module.__file__) transcript_file = os.path.join(test_dir, 'transcripts', filename) @@ -155,7 +150,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): assert err.endswith(expected_end) -def test_history_transcript(): +def test_history_transcript() -> None: app = CmdLineApp() app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') @@ -174,7 +169,7 @@ def test_history_transcript(): os.close(fd) # tell the history command to create a transcript - run_cmd(app, 'history -t "{}"'.format(history_fname)) + run_cmd(app, f'history -t "{history_fname}"') # read in the transcript created by the history command with open(history_fname) as f: @@ -183,7 +178,7 @@ def test_history_transcript(): assert xscript == expected -def test_history_transcript_bad_path(mocker): +def test_history_transcript_bad_path(mocker) -> None: app = CmdLineApp() app.stdout = StdSim(app.stdout) run_cmd(app, 'orate this is\na /multiline/\ncommand;\n') @@ -191,7 +186,7 @@ def test_history_transcript_bad_path(mocker): # Bad directory history_fname = '~/fakedir/this_does_not_exist.txt' - out, err = run_cmd(app, 'history -t "{}"'.format(history_fname)) + out, err = run_cmd(app, f'history -t "{history_fname}"') assert "is not a directory" in err[0] # Cause os.open to fail and make sure error gets printed @@ -199,11 +194,11 @@ def test_history_transcript_bad_path(mocker): mock_remove.side_effect = OSError history_fname = 'outfile.txt' - out, err = run_cmd(app, 'history -t "{}"'.format(history_fname)) + out, err = run_cmd(app, f'history -t "{history_fname}"') assert "Error saving transcript file" in err[0] -def test_run_script_record_transcript(base_app, request): +def test_run_script_record_transcript(base_app, request) -> None: test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'help.txt') @@ -215,7 +210,7 @@ def test_run_script_record_transcript(base_app, request): os.close(fd) # Execute the run_script command with the -t option to generate a transcript - run_cmd(base_app, 'run_script {} -t {}'.format(filename, transcript_fname)) + run_cmd(base_app, f'run_script {filename} -t {transcript_fname}') assert base_app._script_dir == [] assert base_app._current_script_dir is None @@ -228,7 +223,7 @@ def test_run_script_record_transcript(base_app, request): verify_help_text(base_app, xscript) -def test_generate_transcript_stop(capsys): +def test_generate_transcript_stop(capsys) -> None: # Verify transcript generation stops when a command returns True for stop app = CmdLineApp() @@ -256,7 +251,7 @@ def test_generate_transcript_stop(capsys): @pytest.mark.parametrize( - 'expected, transformed', + ('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. @@ -279,7 +274,7 @@ def test_generate_transcript_stop(capsys): (r'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff')), ], ) -def test_parse_transcript_expected(expected, transformed): +def test_parse_transcript_expected(expected, transformed) -> None: app = CmdLineApp() class TestMyAppCase(transcript.Cmd2TestCase): @@ -289,7 +284,7 @@ class TestMyAppCase(transcript.Cmd2TestCase): assert testcase._transform_transcript_expected(expected) == transformed -def test_transcript_failure(request, capsys): +def test_transcript_failure(request, capsys) -> None: # Get location of the transcript test_dir = os.path.dirname(request.module.__file__) transcript_file = os.path.join(test_dir, 'transcripts', 'failure.txt') @@ -315,7 +310,7 @@ def test_transcript_failure(request, capsys): assert err.endswith(expected_end) -def test_transcript_no_file(request, capsys): +def test_transcript_no_file(request, capsys) -> None: # Need to patch sys.argv so cmd2 doesn't think it was called with # arguments equal to the py.test args testargs = ['prog', '-t'] diff --git a/tests/test_utils.py b/tests/test_utils.py index a173f7f45..bb05093ea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit testing for cmd2/utils.py module. -""" +"""Unit testing for cmd2/utils.py module.""" import os import signal @@ -26,50 +22,50 @@ HELLO_WORLD = 'Hello, world!' -def test_strip_quotes_no_quotes(): +def test_strip_quotes_no_quotes() -> None: base_str = HELLO_WORLD stripped = cu.strip_quotes(base_str) assert base_str == stripped -def test_strip_quotes_with_quotes(): +def test_strip_quotes_with_quotes() -> None: base_str = '"' + HELLO_WORLD + '"' stripped = cu.strip_quotes(base_str) assert stripped == HELLO_WORLD -def test_remove_duplicates_no_duplicates(): +def test_remove_duplicates_no_duplicates() -> None: no_dups = [5, 4, 3, 2, 1] assert cu.remove_duplicates(no_dups) == no_dups -def test_remove_duplicates_with_duplicates(): +def test_remove_duplicates_with_duplicates() -> None: duplicates = [1, 1, 2, 3, 9, 9, 7, 8] assert cu.remove_duplicates(duplicates) == [1, 2, 3, 9, 7, 8] -def test_unicode_normalization(): +def test_unicode_normalization() -> None: s1 = 'café' s2 = 'cafe\u0301' assert s1 != s2 assert cu.norm_fold(s1) == cu.norm_fold(s2) -def test_unicode_casefold(): +def test_unicode_casefold() -> None: micro = 'µ' micro_cf = micro.casefold() assert micro != micro_cf assert cu.norm_fold(micro) == cu.norm_fold(micro_cf) -def test_alphabetical_sort(): +def test_alphabetical_sort() -> None: 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(): +def test_try_int_or_force_to_lower_case() -> None: str1 = '17' assert cu.try_int_or_force_to_lower_case(str1) == 17 str1 = 'ABC' @@ -80,7 +76,7 @@ def test_try_int_or_force_to_lower_case(): assert cu.try_int_or_force_to_lower_case(str1) == '' -def test_natural_keys(): +def test_natural_keys() -> None: my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22'] my_list.sort(key=cu.natural_keys) assert my_list == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ'] @@ -89,28 +85,28 @@ def test_natural_keys(): assert my_list == ['a1', 'A2', 'a3', 'A11', 'a22'] -def test_natural_sort(): +def test_natural_sort() -> None: 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(): +def test_is_quoted_short() -> None: my_str = '' assert not cu.is_quoted(my_str) your_str = '"' assert not cu.is_quoted(your_str) -def test_is_quoted_yes(): +def test_is_quoted_yes() -> None: 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(): +def test_is_quoted_no() -> None: my_str = '"This is a test' assert not cu.is_quoted(my_str) your_str = "of the emergengy broadcast system'" @@ -119,7 +115,7 @@ def test_is_quoted_no(): assert not cu.is_quoted(simple_str) -def test_quote_string(): +def test_quote_string() -> None: my_str = "Hello World" assert cu.quote_string(my_str) == '"' + my_str + '"' @@ -130,14 +126,14 @@ def test_quote_string(): assert cu.quote_string(my_str) == "'" + my_str + "'" -def test_quote_string_if_needed_yes(): +def test_quote_string_if_needed_yes() -> None: 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(): +def test_quote_string_if_needed_no() -> None: my_str = "HelloWorld" assert cu.quote_string_if_needed(my_str) == my_str your_str = "'Hello World'" @@ -146,36 +142,35 @@ def test_quote_string_if_needed_no(): @pytest.fixture def stdout_sim(): - stdsim = cu.StdSim(sys.stdout, echo=True) - return stdsim + return cu.StdSim(sys.stdout, echo=True) -def test_stdsim_write_str(stdout_sim): +def test_stdsim_write_str(stdout_sim) -> None: my_str = 'Hello World' stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str -def test_stdsim_write_bytes(stdout_sim): +def test_stdsim_write_bytes(stdout_sim) -> None: b_str = b'Hello World' with pytest.raises(TypeError): stdout_sim.write(b_str) -def test_stdsim_buffer_write_bytes(stdout_sim): +def test_stdsim_buffer_write_bytes(stdout_sim) -> None: 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): +def test_stdsim_buffer_write_str(stdout_sim) -> None: my_str = 'Hello World' with pytest.raises(TypeError): stdout_sim.buffer.write(my_str) -def test_stdsim_read(stdout_sim): +def test_stdsim_read(stdout_sim) -> None: my_str = 'Hello World' stdout_sim.write(my_str) # getvalue() returns the value and leaves it unaffected internally @@ -191,7 +186,7 @@ def test_stdsim_read(stdout_sim): assert stdout_sim.getvalue() == my_str[2:] -def test_stdsim_read_bytes(stdout_sim): +def test_stdsim_read_bytes(stdout_sim) -> None: b_str = b'Hello World' stdout_sim.buffer.write(b_str) # getbytes() returns the value and leaves it unaffected internally @@ -201,7 +196,7 @@ def test_stdsim_read_bytes(stdout_sim): assert stdout_sim.getbytes() == b'' -def test_stdsim_clear(stdout_sim): +def test_stdsim_clear(stdout_sim) -> None: my_str = 'Hello World' stdout_sim.write(my_str) assert stdout_sim.getvalue() == my_str @@ -209,7 +204,7 @@ def test_stdsim_clear(stdout_sim): assert stdout_sim.getvalue() == '' -def test_stdsim_getattr_exist(stdout_sim): +def test_stdsim_getattr_exist(stdout_sim) -> None: # Here the StdSim getattr is allowing us to access methods within StdSim my_str = 'Hello World' stdout_sim.write(my_str) @@ -217,12 +212,12 @@ def test_stdsim_getattr_exist(stdout_sim): assert val_func() == my_str -def test_stdsim_getattr_noexist(stdout_sim): +def test_stdsim_getattr_noexist(stdout_sim) -> None: # 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): +def test_stdsim_pause_storage(stdout_sim) -> None: # Test pausing storage for string data my_str = 'Hello World' @@ -246,7 +241,7 @@ def test_stdsim_pause_storage(stdout_sim): assert stdout_sim.getbytes() == b'' -def test_stdsim_line_buffering(base_app): +def test_stdsim_line_buffering(base_app) -> None: # 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 @@ -274,7 +269,7 @@ def pr_none(): # Start a long running process so we have time to run tests on it before it finishes # Put the new process into a separate group so its signal are isolated from ours - kwargs = dict() + kwargs = {} if sys.platform.startswith('win'): command = 'timeout -t 5 /nobreak' kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP @@ -283,11 +278,10 @@ def pr_none(): kwargs['start_new_session'] = True proc = subprocess.Popen(command, shell=True, **kwargs) - pr = cu.ProcReader(proc, None, None) - return pr + return cu.ProcReader(proc, None, None) -def test_proc_reader_send_sigint(pr_none): +def test_proc_reader_send_sigint(pr_none) -> None: assert pr_none._proc.poll() is None pr_none.send_sigint() pr_none.wait() @@ -300,7 +294,7 @@ def test_proc_reader_send_sigint(pr_none): assert ret_code == -signal.SIGINT -def test_proc_reader_terminate(pr_none): +def test_proc_reader_terminate(pr_none) -> None: assert pr_none._proc.poll() is None pr_none.terminate() @@ -324,22 +318,18 @@ def context_flag(): return cu.ContextFlag() -def test_context_flag_bool(context_flag): +def test_context_flag_bool(context_flag) -> None: assert not context_flag with context_flag: assert context_flag -def test_context_flag_exit_err(context_flag): - with pytest.raises(ValueError): +def test_context_flag_exit_err(context_flag) -> None: + with pytest.raises(ValueError, match="count has gone below 0"): context_flag.__exit__() -def test_remove_overridden_styles(): - from typing import ( - List, - ) - +def test_remove_overridden_styles() -> None: from cmd2 import ( Bg, EightBitBg, @@ -350,7 +340,7 @@ def test_remove_overridden_styles(): TextStyle, ) - def make_strs(styles_list: List[ansi.AnsiSequence]) -> List[str]: + def make_strs(styles_list: list[ansi.AnsiSequence]) -> list[str]: return [str(s) for s in styles_list] # Test Reset All @@ -408,42 +398,42 @@ def make_strs(styles_list: List[ansi.AnsiSequence]) -> List[str]: assert cu._remove_overridden_styles(styles_to_parse) == expected -def test_truncate_line(): +def test_truncate_line() -> None: line = 'long' max_width = 3 truncated = cu.truncate_line(line, max_width) assert truncated == 'lo' + HORIZONTAL_ELLIPSIS -def test_truncate_line_already_fits(): +def test_truncate_line_already_fits() -> None: line = 'long' max_width = 4 truncated = cu.truncate_line(line, max_width) assert truncated == line -def test_truncate_line_with_newline(): +def test_truncate_line_with_newline() -> None: line = 'fo\no' max_width = 2 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="text contains an unprintable character"): cu.truncate_line(line, max_width) -def test_truncate_line_width_is_too_small(): +def test_truncate_line_width_is_too_small() -> None: line = 'foo' max_width = 0 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="max_width must be at least 1"): cu.truncate_line(line, max_width) -def test_truncate_line_wide_text(): +def test_truncate_line_wide_text() -> None: line = '苹苹other' max_width = 6 truncated = cu.truncate_line(line, max_width) assert truncated == '苹苹o' + HORIZONTAL_ELLIPSIS -def test_truncate_line_split_wide_text(): +def test_truncate_line_split_wide_text() -> None: """Test when truncation results in a string which is shorter than max_width""" line = '1苹2苹' max_width = 3 @@ -451,14 +441,14 @@ def test_truncate_line_split_wide_text(): assert truncated == '1' + HORIZONTAL_ELLIPSIS -def test_truncate_line_tabs(): +def test_truncate_line_tabs() -> None: line = 'has\ttab' max_width = 9 truncated = cu.truncate_line(line, max_width) assert truncated == 'has t' + HORIZONTAL_ELLIPSIS -def test_truncate_with_style(): +def test_truncate_with_style() -> None: from cmd2 import ( Fg, TextStyle, @@ -490,7 +480,7 @@ def test_truncate_with_style(): assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + filtered_after_text -def test_align_text_fill_char_is_tab(): +def test_align_text_fill_char_is_tab() -> None: text = 'foo' fill_char = '\t' width = 5 @@ -498,7 +488,7 @@ def test_align_text_fill_char_is_tab(): assert aligned == text + ' ' -def test_align_text_with_style(): +def test_align_text_with_style() -> None: from cmd2 import ( Fg, TextStyle, @@ -546,15 +536,15 @@ def test_align_text_with_style(): 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(): +def test_align_text_width_is_too_small() -> None: text = 'foo' fill_char = '-' width = 0 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="width must be at least 1"): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) -def test_align_text_fill_char_is_too_long(): +def test_align_text_fill_char_is_too_long() -> None: text = 'foo' fill_char = 'fill' width = 5 @@ -562,15 +552,15 @@ def test_align_text_fill_char_is_too_long(): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) -def test_align_text_fill_char_is_newline(): +def test_align_text_fill_char_is_newline() -> None: text = 'foo' fill_char = '\n' width = 5 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Fill character is an unprintable character"): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) -def test_align_text_has_tabs(): +def test_align_text_has_tabs() -> None: text = '\t\tfoo' fill_char = '-' width = 10 @@ -578,7 +568,7 @@ def test_align_text_has_tabs(): assert aligned == ' ' + 'foo' + '---' -def test_align_text_blank(): +def test_align_text_blank() -> None: text = '' fill_char = '-' width = 5 @@ -586,7 +576,7 @@ def test_align_text_blank(): assert aligned == fill_char * width -def test_align_text_wider_than_width(): +def test_align_text_wider_than_width() -> None: text = 'long text field' fill_char = '-' width = 8 @@ -594,7 +584,7 @@ def test_align_text_wider_than_width(): assert aligned == text -def test_align_text_wider_than_width_truncate(): +def test_align_text_wider_than_width_truncate() -> None: text = 'long text field' fill_char = '-' width = 8 @@ -602,7 +592,7 @@ def test_align_text_wider_than_width_truncate(): assert aligned == 'long te' + HORIZONTAL_ELLIPSIS -def test_align_text_wider_than_width_truncate_add_fill(): +def test_align_text_wider_than_width_truncate_add_fill() -> None: """Test when truncation results in a string which is shorter than width and align_text adds filler""" text = '1苹2苹' fill_char = '-' @@ -611,15 +601,15 @@ def test_align_text_wider_than_width_truncate_add_fill(): assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char -def test_align_text_has_unprintable(): +def test_align_text_has_unprintable() -> None: text = 'foo\x02' fill_char = '-' width = 5 - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Text to align contains an unprintable character"): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) -def test_align_text_term_width(): +def test_align_text_term_width() -> None: import shutil text = 'foo' @@ -634,7 +624,7 @@ def test_align_text_term_width(): assert aligned == text + expected_fill -def test_align_left(): +def test_align_left() -> None: text = 'foo' fill_char = '-' width = 5 @@ -642,7 +632,7 @@ def test_align_left(): assert aligned == text + fill_char + fill_char -def test_align_left_multiline(): +def test_align_left_multiline() -> None: # Without style text = "foo\nshoes" fill_char = '-' @@ -667,7 +657,7 @@ def test_align_left_multiline(): assert aligned == expected -def test_align_left_wide_text(): +def test_align_left_wide_text() -> None: text = '苹' fill_char = '-' width = 4 @@ -675,7 +665,7 @@ def test_align_left_wide_text(): assert aligned == text + fill_char + fill_char -def test_align_left_wide_fill(): +def test_align_left_wide_fill() -> None: text = 'foo' fill_char = '苹' width = 5 @@ -683,7 +673,7 @@ def test_align_left_wide_fill(): assert aligned == text + fill_char -def test_align_left_wide_fill_needs_padding(): +def test_align_left_wide_fill_needs_padding() -> None: """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' @@ -692,7 +682,7 @@ def test_align_left_wide_fill_needs_padding(): assert aligned == text + fill_char + ' ' -def test_align_center(): +def test_align_center() -> None: text = 'foo' fill_char = '-' width = 5 @@ -700,7 +690,7 @@ def test_align_center(): assert aligned == fill_char + text + fill_char -def test_align_center_multiline(): +def test_align_center_multiline() -> None: # Without style text = "foo\nshoes" fill_char = '-' @@ -725,7 +715,7 @@ def test_align_center_multiline(): assert aligned == expected -def test_align_center_wide_text(): +def test_align_center_wide_text() -> None: text = '苹' fill_char = '-' width = 4 @@ -733,7 +723,7 @@ def test_align_center_wide_text(): assert aligned == fill_char + text + fill_char -def test_align_center_wide_fill(): +def test_align_center_wide_fill() -> None: text = 'foo' fill_char = '苹' width = 7 @@ -741,7 +731,7 @@ def test_align_center_wide_fill(): assert aligned == fill_char + text + fill_char -def test_align_center_wide_fill_needs_right_padding(): +def test_align_center_wide_fill_needs_right_padding() -> None: """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' fill_char = '苹' @@ -750,7 +740,7 @@ def test_align_center_wide_fill_needs_right_padding(): assert aligned == fill_char + text + fill_char + ' ' -def test_align_center_wide_fill_needs_left_and_right_padding(): +def test_align_center_wide_fill_needs_left_and_right_padding() -> None: """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' fill_char = '苹' @@ -759,7 +749,7 @@ def test_align_center_wide_fill_needs_left_and_right_padding(): assert aligned == fill_char + ' ' + text + fill_char + ' ' -def test_align_right(): +def test_align_right() -> None: text = 'foo' fill_char = '-' width = 5 @@ -767,7 +757,7 @@ def test_align_right(): assert aligned == fill_char + fill_char + text -def test_align_right_multiline(): +def test_align_right_multiline() -> None: # Without style text = "foo\nshoes" fill_char = '-' @@ -792,7 +782,7 @@ def test_align_right_multiline(): assert aligned == expected -def test_align_right_wide_text(): +def test_align_right_wide_text() -> None: text = '苹' fill_char = '-' width = 4 @@ -800,7 +790,7 @@ def test_align_right_wide_text(): assert aligned == fill_char + fill_char + text -def test_align_right_wide_fill(): +def test_align_right_wide_fill() -> None: text = 'foo' fill_char = '苹' width = 5 @@ -808,7 +798,7 @@ def test_align_right_wide_fill(): assert aligned == fill_char + text -def test_align_right_wide_fill_needs_padding(): +def test_align_right_wide_fill_needs_padding() -> None: """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' @@ -817,51 +807,51 @@ def test_align_right_wide_fill_needs_padding(): assert aligned == fill_char + ' ' + text -def test_to_bool_str_true(): +def test_to_bool_str_true() -> None: assert cu.to_bool('true') assert cu.to_bool('True') assert cu.to_bool('TRUE') assert cu.to_bool('tRuE') -def test_to_bool_str_false(): +def test_to_bool_str_false() -> None: assert not cu.to_bool('false') assert not cu.to_bool('False') assert not cu.to_bool('FALSE') assert not cu.to_bool('fAlSe') -def test_to_bool_str_invalid(): - with pytest.raises(ValueError): +def test_to_bool_str_invalid() -> None: + with pytest.raises(ValueError): # noqa: PT011 cu.to_bool('other') -def test_to_bool_bool(): +def test_to_bool_bool() -> None: assert cu.to_bool(True) assert not cu.to_bool(False) -def test_to_bool_int(): +def test_to_bool_int() -> None: assert cu.to_bool(1) assert cu.to_bool(-1) assert not cu.to_bool(0) -def test_to_bool_float(): +def test_to_bool_float() -> None: assert cu.to_bool(2.35) assert cu.to_bool(0.25) assert cu.to_bool(-3.1415) assert not cu.to_bool(0) -def test_find_editor_specified(): +def test_find_editor_specified() -> None: 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(): +def test_find_editor_not_specified() -> None: # Use existing path env setting. Something in the editor list should be found. editor = cu.find_editor() assert editor @@ -872,26 +862,26 @@ def test_find_editor_not_specified(): assert editor is None -def test_similarity(): +def test_similarity() -> None: suggested_command = cu.suggest_similar("comand", ["command", "UNRELATED", "NOT_SIMILAR"]) assert suggested_command == "command" suggested_command = cu.suggest_similar("command", ["COMMAND", "acommands"]) assert suggested_command == "COMMAND" -def test_similarity_without_good_canididates(): +def test_similarity_without_good_canididates() -> None: suggested_command = cu.suggest_similar("comand", ["UNRELATED", "NOT_SIMILAR"]) assert suggested_command is None suggested_command = cu.suggest_similar("comand", []) assert suggested_command is None -def test_similarity_overwrite_function(): +def test_similarity_overwrite_function() -> None: options = ["history", "test"] suggested_command = cu.suggest_similar("test", options) assert suggested_command == 'test' - def custom_similarity_function(s1, s2): + def custom_similarity_function(s1, s2) -> float: return 1.0 if 'history' in (s1, s2) else 0.0 suggested_command = cu.suggest_similar("test", options, similarity_function_to_use=custom_similarity_function) diff --git a/tests/test_utils_defining_class.py b/tests/test_utils_defining_class.py index 8b6ede8bf..f0c278957 100644 --- a/tests/test_utils_defining_class.py +++ b/tests/test_utils_defining_class.py @@ -1,48 +1,45 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Unit testing for get_defining_class in cmd2/utils.py module. -""" +"""Unit testing for get_defining_class in cmd2/utils.py module.""" import functools import cmd2.utils as cu -class ParentClass(object): - def func_with_overrides(self): +class ParentClass: + def func_with_overrides(self) -> None: pass - def parent_only_func(self, param1, param2): + def parent_only_func(self, param1, param2) -> None: pass class ChildClass(ParentClass): - def func_with_overrides(self): - super(ChildClass, self).func_with_overrides() + def func_with_overrides(self) -> None: + super().func_with_overrides() - def child_function(self): + def child_function(self) -> None: pass - def lambda1(): + def lambda1() -> int: return 1 - lambda2 = (lambda: lambda: 2)() + def lambda2() -> int: + return 2 @classmethod - def class_method(cls): + def class_method(cls) -> None: pass @staticmethod - def static_meth(): + def static_meth() -> None: pass -def func_not_in_class(): +def func_not_in_class() -> None: pass -def test_get_defining_class(): +def test_get_defining_class() -> None: parent_instance = ParentClass() child_instance = ChildClass() diff --git a/tests_isolated/test_commandset/__init__.py b/tests_isolated/test_commandset/__init__.py index 037f3866e..e69de29bb 100644 --- a/tests_isolated/test_commandset/__init__.py +++ b/tests_isolated/test_commandset/__init__.py @@ -1,3 +0,0 @@ -# -# -*- coding: utf-8 -*- -# diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index c8c6d34b4..d5853de4a 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -1,7 +1,4 @@ -# coding=utf-8 -""" -Cmd2 unit/functional testing -""" +"""Cmd2 unit/functional testing""" import sys from contextlib import ( @@ -9,7 +6,6 @@ redirect_stdout, ) from typing import ( - List, Optional, Union, ) @@ -17,12 +13,10 @@ mock, ) +import pytest from cmd2_ext_test import ( ExternalTestMixin, ) -from pytest import ( - fixture, -) import cmd2 from cmd2.rl_utils import ( @@ -34,7 +28,7 @@ def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None + 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. @@ -42,10 +36,7 @@ def verify_help_text( :param help_output: output of help, either as a string or list of strings :param verbose_strings: optional list of verbose strings to search for """ - if isinstance(help_output, str): - help_text = help_output - else: - help_text = ''.join(help_output) + help_text = help_output if isinstance(help_output, str) else ''.join(help_output) commands = cmd2_app.get_visible_commands() for command in commands: assert command in help_text @@ -124,9 +115,8 @@ def run_cmd(app, cmd): try: app.stdout = copy_cmd_stdout - with redirect_stdout(copy_cmd_stdout): - with redirect_stderr(copy_stderr): - app.onecmd_plus_hooks(cmd) + with redirect_stdout(copy_cmd_stdout), redirect_stderr(copy_stderr): + app.onecmd_plus_hooks(cmd) finally: app.stdout = copy_cmd_stdout.inner_stream sys.stdout = saved_sysout @@ -136,7 +126,7 @@ def run_cmd(app, cmd): return normalize(out), normalize(err) -@fixture +@pytest.fixture def base_app(): return cmd2.Cmd() @@ -146,8 +136,7 @@ def base_app(): def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: - """ - This is a convenience function to test cmd2.complete() since + """This is a convenience function to test cmd2.complete() since in a unit test environment there is no actual console readline is monitoring. Therefore we use mock to provide readline data to complete(). @@ -172,26 +161,23 @@ def get_endidx(): return endidx # Run the readline tab completion function with readline mocks in place - with mock.patch.object(readline, 'get_line_buffer', get_line): - with mock.patch.object(readline, 'get_begidx', get_begidx): - with mock.patch.object(readline, 'get_endidx', get_endidx): - return app.complete(text, 0) + with mock.patch.object(readline, 'get_line_buffer', get_line), mock.patch.object(readline, 'get_begidx', get_begidx): + with mock.patch.object(readline, 'get_endidx', get_endidx): + return app.complete(text, 0) 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) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) -@fixture +@pytest.fixture def command_sets_app(): - app = WithCommandSets() - return app + return WithCommandSets() -@fixture() +@pytest.fixture def command_sets_manual(): - app = WithCommandSets(auto_load_commands=False) - return app + return WithCommandSets(auto_load_commands=False) diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py index 3a9ab5955..74655de4a 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -1,8 +1,4 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -reproduces test_argparse.py except with SubCommands -""" +"""reproduces test_argparse.py except with SubCommands""" import pytest @@ -17,21 +13,21 @@ class SubcommandSet(cmd2.CommandSet): """Example cmd2 application where we a base command which has a couple subcommands.""" - def __init__(self, dummy): - super(SubcommandSet, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() # subcommand functions for the base command - def base_foo(self, args): - """foo subcommand of base command""" + def base_foo(self, args) -> None: + """Foo subcommand of base command""" self._cmd.poutput(args.x * args.y) - def base_bar(self, args): - """bar subcommand of base command""" - self._cmd.poutput('((%s))' % args.z) + def base_bar(self, args) -> None: + """Bar subcommand of base command""" + self._cmd.poutput(f'(({args.z}))') - def base_helpless(self, args): - """helpless subcommand of base command""" - self._cmd.poutput('((%s))' % args.z) + def base_helpless(self, args) -> None: + """Helpless subcommand of base command""" + self._cmd.poutput(f'(({args.z}))') # create the top-level parser for the base command base_parser = cmd2.Cmd2ArgumentParser() @@ -52,12 +48,12 @@ def base_helpless(self, args): # This subcommand has aliases and no help text. It exists to prevent changes to _set_parser_prog() which # use an approach which relies on action._choices_actions list. See comment in that function for more # details. - parser_bar = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2']) - parser_bar.add_argument('z', help='string') - parser_bar.set_defaults(func=base_bar) + parser_helpless = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2']) + parser_helpless.add_argument('z', help='string') + parser_helpless.set_defaults(func=base_bar) @cmd2.with_argparser(base_parser) - def do_base(self, args): + def do_base(self, args) -> None: """Base command help""" # Call whatever subcommand function was selected func = getattr(args, 'func') @@ -66,34 +62,33 @@ def do_base(self, args): @pytest.fixture def subcommand_app(): - app = WithCommandSets(auto_load_commands=False, command_sets=[SubcommandSet(1)]) - return app + return WithCommandSets(auto_load_commands=False, command_sets=[SubcommandSet(1)]) -def test_subcommand_foo(subcommand_app): +def test_subcommand_foo(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base foo -x2 5.0') assert out == ['10.0'] -def test_subcommand_bar(subcommand_app): +def test_subcommand_bar(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base bar baz') assert out == ['((baz))'] -def test_subcommand_invalid(subcommand_app): +def test_subcommand_invalid(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'base baz') assert err[0].startswith('Usage: base') assert err[1].startswith("Error: argument SUBCOMMAND: invalid choice: 'baz'") -def test_subcommand_base_help(subcommand_app): +def test_subcommand_base_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base') assert out[0].startswith('Usage: base') assert out[1] == '' assert out[2] == 'Base command help' -def test_subcommand_help(subcommand_app): +def test_subcommand_help(subcommand_app) -> None: # foo has no aliases out, err = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('Usage: base foo') @@ -133,6 +128,6 @@ def test_subcommand_help(subcommand_app): assert out[2] == 'positional arguments:' -def test_subcommand_invalid_help(subcommand_app): +def test_subcommand_invalid_help(subcommand_app) -> None: out, err = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('Usage: base') diff --git a/tests_isolated/test_commandset/test_categories.py b/tests_isolated/test_commandset/test_categories.py index 986ae3fa9..8150c5e7d 100644 --- a/tests_isolated/test_commandset/test_categories.py +++ b/tests_isolated/test_commandset/test_categories.py @@ -1,12 +1,6 @@ -#!/usr/bin/env python3 -# coding=utf-8 -""" -Simple example demonstrating basic CommandSet usage. -""" - -from typing import ( - Any, -) +"""Simple example demonstrating basic CommandSet usage.""" + +from typing import Any import cmd2 from cmd2 import ( @@ -19,77 +13,69 @@ class MyBaseCommandSet(CommandSet): """Defines a default category for all sub-class CommandSets""" - def __init__(self, _: Any): - super(MyBaseCommandSet, self).__init__() + def __init__(self, _: Any) -> None: + super().__init__() class ChildInheritsParentCategories(MyBaseCommandSet): - """ - This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category' - """ + """This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category'""" - def do_hello(self, _: cmd2.Statement): + def do_hello(self, _: cmd2.Statement) -> None: self._cmd.poutput('Hello') - def do_world(self, _: cmd2.Statement): + def do_world(self, _: cmd2.Statement) -> None: self._cmd.poutput('World') @with_default_category('Non-Heritable Category', heritable=False) class ChildOverridesParentCategoriesNonHeritable(MyBaseCommandSet): - """ - This subclass overrides the 'Default Category' from the parent, but in a non-heritable fashion. Sub-classes of this + """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): + def do_goodbye(self, _: cmd2.Statement) -> None: self._cmd.poutput('Goodbye') class GrandchildInheritsGrandparentCategory(ChildOverridesParentCategoriesNonHeritable): - """ - This subclass's parent class declared its default category non-heritable. Instead, it inherits the category defined + """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): + def do_aloha(self, _: cmd2.Statement) -> None: self._cmd.poutput('Aloha') @with_default_category('Heritable Category') class ChildOverridesParentCategories(MyBaseCommandSet): - """ - This subclass is decorated with a default category that is heritable. This overrides the parent class's default + """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): + def do_bonjour(self, _: cmd2.Statement) -> None: self._cmd.poutput('Bonjour') class GrandchildInheritsHeritable(ChildOverridesParentCategories): - """ - This subclass's parent declares a default category that overrides its parent. As a result, commands in this + """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): + def do_monde(self, _: cmd2.Statement) -> None: self._cmd.poutput('Monde') class ExampleApp(cmd2.Cmd): - """ - Example to demonstrate heritable default categories - """ + """Example to demonstrate heritable default categories""" - def __init__(self): - super(ExampleApp, self).__init__(auto_load_commands=False) + def __init__(self) -> None: + super().__init__(auto_load_commands=False) - def do_something(self, arg): + def do_something(self, arg) -> None: self.poutput('this is the something command') -def test_heritable_categories(): +def test_heritable_categories() -> None: app = ExampleApp() base_cs = MyBaseCommandSet(0) diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 89c982f39..b8daafa04 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -1,14 +1,7 @@ -# coding=utf-8 -# flake8: noqa E302 -""" -Test CommandSet -""" +"""Test CommandSet""" import argparse import signal -from typing import ( - List, -) import pytest @@ -50,10 +43,10 @@ def on_unregistered(self) -> None: super().on_unregistered() print("in on_unregistered now") - def do_apple(self, statement: cmd2.Statement): + def do_apple(self, statement: cmd2.Statement) -> None: self._cmd.poutput('Apple!') - def do_banana(self, statement: cmd2.Statement): + def do_banana(self, statement: cmd2.Statement) -> None: """Banana Command""" self._cmd.poutput('Banana!!') @@ -61,24 +54,24 @@ def do_banana(self, statement: cmd2.Statement): cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce']) @cmd2.with_argparser(cranberry_parser, with_unknown_args=True) - def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]): - self._cmd.poutput('Cranberry {}!!'.format(ns.arg1)) + def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]) -> None: + self._cmd.poutput(f'Cranberry {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} - def help_cranberry(self): + def help_cranberry(self) -> None: self._cmd.stdout.write('This command does diddly squat...\n') @cmd2.with_argument_list @cmd2.with_category('Also Alone') - def do_durian(self, args: List[str]): + def do_durian(self, args: list[str]) -> None: """Durian Command""" - self._cmd.poutput('{} Arguments: '.format(len(args))) + self._cmd.poutput(f'{len(args)} Arguments: ') 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]: + def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting']) elderberry_parser = cmd2.Cmd2ArgumentParser() @@ -86,8 +79,8 @@ def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> Lis @cmd2.with_category('Alone') @cmd2.with_argparser(elderberry_parser) - def do_elderberry(self, ns: argparse.Namespace): - self._cmd.poutput('Elderberry {}!!'.format(ns.arg1)) + def do_elderberry(self, ns: argparse.Namespace) -> None: + self._cmd.poutput(f'Elderberry {ns.arg1}!!') self._cmd.last_result = {'arg1': ns.arg1} # Test that CommandSet with as_subcommand_to decorator successfully loads @@ -112,22 +105,22 @@ def subcmd_func(self, args: argparse.Namespace) -> None: @cmd2.with_default_category('Command Set B') class CommandSetB(CommandSetBase): - def __init__(self, arg1): + def __init__(self, arg1) -> None: super().__init__() self._arg1 = arg1 - def do_aardvark(self, statement: cmd2.Statement): + def do_aardvark(self, statement: cmd2.Statement) -> None: self._cmd.poutput('Aardvark!') - def do_bat(self, statement: cmd2.Statement): + def do_bat(self, statement: cmd2.Statement) -> None: """Banana Command""" self._cmd.poutput('Bat!!') - def do_crocodile(self, statement: cmd2.Statement): + def do_crocodile(self, statement: cmd2.Statement) -> None: self._cmd.poutput('Crocodile!!') -def test_autoload_commands(command_sets_app): +def test_autoload_commands(command_sets_app) -> None: # verifies that, when autoload is enabled, CommandSets and registered functions all show up cmds_cats, cmds_doc, cmds_undoc, help_topics = command_sets_app._build_command_info() @@ -149,16 +142,16 @@ def test_autoload_commands(command_sets_app): assert 'Command Set B' not in cmds_cats -def test_command_synonyms(): +def test_command_synonyms() -> None: """Test the use of command synonyms in CommandSets""" class SynonymCommandSet(cmd2.CommandSet): - def __init__(self, arg1): + def __init__(self, arg1) -> None: super().__init__() self._arg1 = arg1 @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(description="Native Command")) - def do_builtin(self, _): + def do_builtin(self, _) -> None: pass # Create a synonym to a command inside of this CommandSet @@ -195,7 +188,7 @@ def do_builtin(self, _): assert normalize(alias_parser.format_help())[0] in out -def test_custom_construct_commandsets(): +def test_custom_construct_commandsets() -> None: command_set_b = CommandSetB('foo') # Verify that _cmd cannot be accessed until CommandSet is registered. @@ -237,7 +230,7 @@ def test_custom_construct_commandsets(): assert command_set_2 not in matches -def test_load_commands(command_sets_manual, capsys): +def test_load_commands(command_sets_manual, capsys) -> None: # now install a command set and verify the commands are now present cmd_set = CommandSetA() @@ -303,7 +296,7 @@ def test_load_commands(command_sets_manual, capsys): assert 'cranberry' in cmds_cats['Fruits'] -def test_commandset_decorators(command_sets_app): +def test_commandset_decorators(command_sets_app) -> None: result = command_sets_app.app_cmd('cranberry juice extra1 extra2') assert result is not None assert result.data is not None @@ -333,7 +326,7 @@ def test_commandset_decorators(command_sets_app): assert result.data is None -def test_load_commandset_errors(command_sets_manual, capsys): +def test_load_commandset_errors(command_sets_manual, capsys) -> None: cmd_set = CommandSetA() # create a conflicting command before installing CommandSet to verify rollback behavior @@ -388,8 +381,8 @@ def test_load_commandset_errors(command_sets_manual, capsys): class LoadableBase(cmd2.CommandSet): - def __init__(self, dummy): - super(LoadableBase, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload self._cut_called = False @@ -402,7 +395,7 @@ def namespace_provider(self) -> argparse.Namespace: return ns @cmd2.with_argparser(cut_parser) - def do_cut(self, ns: argparse.Namespace): + def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" handler = ns.cmd2_handler.get() if handler is not None: @@ -418,7 +411,7 @@ def do_cut(self, ns: argparse.Namespace): stir_subparsers = stir_parser.add_subparsers(title='item', help='what to stir') @cmd2.with_argparser(stir_parser, ns_provider=namespace_provider) - def do_stir(self, ns: argparse.Namespace): + def do_stir(self, ns: argparse.Namespace) -> None: """Stir something""" if not ns.cut_called: self._cmd.poutput('Need to cut before stirring') @@ -438,7 +431,7 @@ def do_stir(self, ns: argparse.Namespace): stir_pasta_parser.add_subparsers(title='style', help='Stir style') @cmd2.as_subcommand_to('stir', 'pasta', stir_pasta_parser) - def stir_pasta(self, ns: argparse.Namespace): + def stir_pasta(self, ns: argparse.Namespace) -> None: handler = ns.cmd2_handler.get() if handler is not None: # Call whatever subcommand function was selected @@ -448,11 +441,11 @@ def stir_pasta(self, ns: argparse.Namespace): class LoadableBadBase(cmd2.CommandSet): - def __init__(self, dummy): - super(LoadableBadBase, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload - def do_cut(self, ns: argparse.Namespace): + def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" handler = ns.cmd2_handler.get() if handler is not None: @@ -466,56 +459,56 @@ def do_cut(self, ns: argparse.Namespace): @cmd2.with_default_category('Fruits') class LoadableFruits(cmd2.CommandSet): - def __init__(self, dummy): - super(LoadableFruits, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload - def do_apple(self, _: cmd2.Statement): + def do_apple(self, _: cmd2.Statement) -> None: self._cmd.poutput('Apple') banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) - def cut_banana(self, ns: argparse.Namespace): + def cut_banana(self, ns: argparse.Namespace) -> None: """Cut banana""" self._cmd.poutput('cutting banana: ' + ns.direction) class LoadablePastaStir(cmd2.CommandSet): - def __init__(self, dummy): - super(LoadablePastaStir, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser() stir_pasta_vigor_parser.add_argument('frequency') @cmd2.as_subcommand_to('stir pasta', 'vigorously', stir_pasta_vigor_parser) - def stir_pasta_vigorously(self, ns: argparse.Namespace): + def stir_pasta_vigorously(self, ns: argparse.Namespace) -> None: self._cmd.poutput('stir the pasta vigorously') @cmd2.with_default_category('Vegetables') class LoadableVegetables(cmd2.CommandSet): - def __init__(self, dummy): - super(LoadableVegetables, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload - def do_arugula(self, _: cmd2.Statement): + def do_arugula(self, _: cmd2.Statement) -> None: self._cmd.poutput('Arugula') - def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return ['quartered', 'diced'] bokchoy_parser = cmd2.Cmd2ArgumentParser() bokchoy_parser.add_argument('style', completer=complete_style_arg) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) - def cut_bokchoy(self, ns: argparse.Namespace): + def cut_bokchoy(self, ns: argparse.Namespace) -> None: self._cmd.poutput('Bok Choy: ' + ns.style) -def test_subcommands(command_sets_manual): +def test_subcommands(command_sets_manual) -> None: base_cmds = LoadableBase(1) badbase_cmds = LoadableBadBase(1) fruit_cmds = LoadableFruits(1) @@ -553,27 +546,27 @@ def test_subcommands(command_sets_manual): assert 'Fruits' in cmds_cats text = '' - line = 'cut {}'.format(text) + line = f'cut {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is not None # check that the alias shows up correctly - assert ['banana', 'bananer', 'bokchoy'] == command_sets_manual.completion_matches + assert command_sets_manual.completion_matches == ['banana', 'bananer', 'bokchoy'] cmd_result = command_sets_manual.app_cmd('cut banana discs') assert 'cutting banana: discs' in cmd_result.stdout text = '' - line = 'cut bokchoy {}'.format(text) + line = f'cut bokchoy {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is not None # verify that argparse completer in commandset functions correctly - assert ['diced', 'quartered'] == command_sets_manual.completion_matches + assert command_sets_manual.completion_matches == ['diced', 'quartered'] # verify that command set uninstalls without problems command_sets_manual.unregister_command_set(fruit_cmds) @@ -598,24 +591,24 @@ def test_subcommands(command_sets_manual): assert 'Fruits' in cmds_cats text = '' - line = 'cut {}'.format(text) + line = f'cut {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is not None # check that the alias shows up correctly - assert ['banana', 'bananer', 'bokchoy'] == command_sets_manual.completion_matches + assert command_sets_manual.completion_matches == ['banana', 'bananer', 'bokchoy'] text = '' - line = 'cut bokchoy {}'.format(text) + line = f'cut bokchoy {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) assert first_match is not None # verify that argparse completer in commandset functions correctly - assert ['diced', 'quartered'] == command_sets_manual.completion_matches + assert command_sets_manual.completion_matches == ['diced', 'quartered'] # disable again and verify can still uninstnall command_sets_manual.disable_command('cut', 'disabled for test') @@ -636,11 +629,11 @@ def test_subcommands(command_sets_manual): command_sets_manual.unregister_command_set(base_cmds) -def test_commandset_sigint(command_sets_manual): +def test_commandset_sigint(command_sets_manual) -> None: # shows that the command is able to continue execution if the sigint_handler # returns True that we've handled interrupting the command. class SigintHandledCommandSet(cmd2.CommandSet): - def do_foo(self, _): + def do_foo(self, _) -> None: self._cmd.poutput('in foo') self._cmd.sigint_handler(signal.SIGINT, None) self._cmd.poutput('end of foo') @@ -656,7 +649,7 @@ def sigint_handler(self) -> bool: # shows that the command is interrupted if we don't report we've handled the sigint class SigintUnhandledCommandSet(cmd2.CommandSet): - def do_bar(self, _): + def do_bar(self, _) -> None: self._cmd.poutput('in do bar') self._cmd.sigint_handler(signal.SIGINT, None) self._cmd.poutput('end of do bar') @@ -668,7 +661,7 @@ def do_bar(self, _): assert 'end of do bar' not in out.stdout -def test_nested_subcommands(command_sets_manual): +def test_nested_subcommands(command_sets_manual) -> None: base_cmds = LoadableBase(1) pasta_cmds = LoadablePastaStir(1) @@ -683,8 +676,8 @@ def test_nested_subcommands(command_sets_manual): command_sets_manual.unregister_command_set(base_cmds) class BadNestedSubcommands(cmd2.CommandSet): - def __init__(self, dummy): - super(BadNestedSubcommands, self).__init__() + def __init__(self, dummy) -> None: + super().__init__() self._dummy = dummy # prevents autoload stir_pasta_vigor_parser = cmd2.Cmd2ArgumentParser() @@ -692,7 +685,7 @@ def __init__(self, dummy): # stir sauce doesn't exist anywhere, this should fail @cmd2.as_subcommand_to('stir sauce', 'vigorously', stir_pasta_vigor_parser) - def stir_pasta_vigorously(self, ns: argparse.Namespace): + def stir_pasta_vigorously(self, ns: argparse.Namespace) -> None: self._cmd.poutput('stir the pasta vigorously') with pytest.raises(CommandSetRegistrationError): @@ -716,14 +709,14 @@ def stir_pasta_vigorously(self, ns: argparse.Namespace): 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) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) cut_parser = cmd2.Cmd2ArgumentParser() cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') @cmd2.with_argparser(cut_parser) - def do_cut(self, ns: argparse.Namespace): + def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" handler = ns.cmd2_handler.get() if handler is not None: @@ -738,50 +731,49 @@ def do_cut(self, ns: argparse.Namespace): banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'banana', banana_parser, help='Cut banana', aliases=['bananer']) - def cut_banana(self, ns: argparse.Namespace): + def cut_banana(self, ns: argparse.Namespace) -> None: """Cut banana""" self.poutput('cutting banana: ' + ns.direction) - def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: return ['quartered', 'diced'] bokchoy_parser = cmd2.Cmd2ArgumentParser() bokchoy_parser.add_argument('style', completer=complete_style_arg) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) - def cut_bokchoy(self, _: argparse.Namespace): + def cut_bokchoy(self, _: argparse.Namespace) -> None: self.poutput('Bok Choy') @pytest.fixture def static_subcommands_app(): - app = AppWithSubCommands() - return app + return AppWithSubCommands() -def test_static_subcommands(static_subcommands_app): +def test_static_subcommands(static_subcommands_app) -> None: cmds_cats, cmds_doc, cmds_undoc, help_topics = static_subcommands_app._build_command_info() assert 'Fruits' in cmds_cats text = '' - line = 'cut {}'.format(text) + line = f'cut {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, static_subcommands_app) assert first_match is not None # check that the alias shows up correctly - assert ['banana', 'bananer', 'bokchoy'] == static_subcommands_app.completion_matches + assert static_subcommands_app.completion_matches == ['banana', 'bananer', 'bokchoy'] text = '' - line = 'cut bokchoy {}'.format(text) + line = f'cut bokchoy {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, static_subcommands_app) assert first_match is not None # verify that argparse completer in commandset functions correctly - assert ['diced', 'quartered'] == static_subcommands_app.completion_matches + assert static_subcommands_app.completion_matches == ['diced', 'quartered'] complete_states_expected_self = None @@ -793,11 +785,11 @@ class SupportFuncProvider(cmd2.CommandSet): states = ['alabama', 'alaska', 'arizona', 'arkansas', 'california', 'colorado', 'connecticut', 'delaware'] - def __init__(self, dummy): - """dummy variable prevents this from being autoloaded in other tests""" - super(SupportFuncProvider, self).__init__() + def __init__(self, dummy) -> None: + """Dummy variable prevents this from being autoloaded in other tests""" + super().__init__() - def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: assert self is complete_states_expected_self return self._cmd.basic_complete(text, line, begidx, endidx, self.states) @@ -809,8 +801,8 @@ class SupportFuncUserSubclass1(SupportFuncProvider): parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states) @cmd2.with_argparser(parser) - def do_user_sub1(self, ns: argparse.Namespace): - self._cmd.poutput('something {}'.format(ns.state)) + def do_user_sub1(self, ns: argparse.Namespace) -> None: + self._cmd.poutput(f'something {ns.state}') class SupportFuncUserSubclass2(SupportFuncProvider): @@ -820,27 +812,27 @@ class SupportFuncUserSubclass2(SupportFuncProvider): parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states) @cmd2.with_argparser(parser) - def do_user_sub2(self, ns: argparse.Namespace): - self._cmd.poutput('something {}'.format(ns.state)) + def do_user_sub2(self, ns: argparse.Namespace) -> None: + self._cmd.poutput(f'something {ns.state}') class SupportFuncUserUnrelated(cmd2.CommandSet): """A CommandSet that isn't related to SupportFuncProvider which uses its support function""" - def __init__(self, dummy): - """dummy variable prevents this from being autoloaded in other tests""" - super(SupportFuncUserUnrelated, self).__init__() + def __init__(self, dummy) -> None: + """Dummy variable prevents this from being autoloaded in other tests""" + super().__init__() parser = cmd2.Cmd2ArgumentParser() parser.add_argument('state', type=str, completer=SupportFuncProvider.complete_states) @cmd2.with_argparser(parser) - def do_user_unrelated(self, ns: argparse.Namespace): - self._cmd.poutput('something {}'.format(ns.state)) + def do_user_unrelated(self, ns: argparse.Namespace) -> None: + self._cmd.poutput(f'something {ns.state}') -def test_cross_commandset_completer(command_sets_manual, capsys): - global complete_states_expected_self +def test_cross_commandset_completer(command_sets_manual, capsys) -> None: + global complete_states_expected_self # noqa: PLW0603 # This tests the different ways to locate the matching CommandSet when completing an argparse argument. # Exercises the 3 cases in cmd2.Cmd._resolve_func_self() which is called during argparse tab completion. @@ -862,7 +854,7 @@ def test_cross_commandset_completer(command_sets_manual, capsys): command_sets_manual.register_command_set(user_sub2) text = '' - line = 'user_sub1 {}'.format(text) + line = f'user_sub1 {text}' endidx = len(line) begidx = endidx complete_states_expected_self = user_sub1 @@ -888,7 +880,7 @@ def test_cross_commandset_completer(command_sets_manual, capsys): command_sets_manual.register_command_set(user_unrelated) text = '' - line = 'user_unrelated {}'.format(text) + line = f'user_unrelated {text}' endidx = len(line) begidx = endidx complete_states_expected_self = func_provider @@ -911,7 +903,7 @@ def test_cross_commandset_completer(command_sets_manual, capsys): command_sets_manual.register_command_set(user_unrelated) text = '' - line = 'user_unrelated {}'.format(text) + line = f'user_unrelated {text}' endidx = len(line) begidx = endidx complete_states_expected_self = user_sub1 @@ -933,7 +925,7 @@ def test_cross_commandset_completer(command_sets_manual, capsys): command_sets_manual.register_command_set(user_unrelated) text = '' - line = 'user_unrelated {}'.format(text) + line = f'user_unrelated {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) @@ -956,7 +948,7 @@ def test_cross_commandset_completer(command_sets_manual, capsys): command_sets_manual.register_command_set(user_unrelated) text = '' - line = 'user_unrelated {}'.format(text) + line = f'user_unrelated {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) @@ -972,25 +964,25 @@ def test_cross_commandset_completer(command_sets_manual, capsys): class CommandSetWithPathComplete(cmd2.CommandSet): - def __init__(self, dummy): - """dummy variable prevents this from being autoloaded in other tests""" - super(CommandSetWithPathComplete, self).__init__() + def __init__(self, dummy) -> None: + """Dummy variable prevents this from being autoloaded in other tests""" + super().__init__() parser = cmd2.Cmd2ArgumentParser() parser.add_argument('path', nargs='+', help='paths', completer=cmd2.Cmd.path_complete) @cmd2.with_argparser(parser) - def do_path(self, app: cmd2.Cmd, args): + def do_path(self, app: cmd2.Cmd, args) -> None: app.poutput(args.path) -def test_path_complete(command_sets_manual): +def test_path_complete(command_sets_manual) -> None: test_set = CommandSetWithPathComplete(1) command_sets_manual.register_command_set(test_set) text = '' - line = 'path {}'.format(text) + line = f'path {text}' endidx = len(line) begidx = endidx first_match = complete_tester(text, line, begidx, endidx, command_sets_manual) @@ -998,26 +990,25 @@ def test_path_complete(command_sets_manual): assert first_match is not None -def test_bad_subcommand(): +def test_bad_subcommand() -> None: class BadSubcommandApp(cmd2.Cmd): """Class for testing usage of `as_subcommand_to` decorator directly in a Cmd2 subclass.""" - def __init__(self, *args, **kwargs): - super(BadSubcommandApp, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) cut_parser = cmd2.Cmd2ArgumentParser() cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') @cmd2.with_argparser(cut_parser) - def do_cut(self, ns: argparse.Namespace): + def do_cut(self, ns: argparse.Namespace) -> None: """Cut something""" - pass banana_parser = cmd2.Cmd2ArgumentParser() banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @cmd2.as_subcommand_to('cut', 'bad name', banana_parser, help='This should fail') - def cut_banana(self, ns: argparse.Namespace): + def cut_banana(self, ns: argparse.Namespace) -> None: """Cut banana""" self.poutput('cutting banana: ' + ns.direction) @@ -1025,16 +1016,16 @@ def cut_banana(self, ns: argparse.Namespace): BadSubcommandApp() -def test_commandset_settables(): +def test_commandset_settables() -> None: # Define an arbitrary class with some attribute class Arbitrary: - def __init__(self): + def __init__(self) -> None: self.some_value = 5 # Declare a CommandSet with a settable of some arbitrary property class WithSettablesA(CommandSetBase): - def __init__(self): - super(WithSettablesA, self).__init__() + def __init__(self) -> None: + super().__init__() self._arbitrary = Arbitrary() self._settable_prefix = 'addon' @@ -1052,8 +1043,8 @@ def __init__(self): # Declare a CommandSet with an empty settable prefix class WithSettablesNoPrefix(CommandSetBase): - def __init__(self): - super(WithSettablesNoPrefix, self).__init__() + def __init__(self) -> None: + super().__init__() self._arbitrary = Arbitrary() self._settable_prefix = '' @@ -1071,8 +1062,8 @@ def __init__(self): # Declare a commandset with duplicate settable name class WithSettablesB(CommandSetBase): - def __init__(self): - super(WithSettablesB, self).__init__() + def __init__(self) -> None: + super().__init__() self._arbitrary = Arbitrary() self._settable_prefix = 'some' @@ -1096,16 +1087,16 @@ def __init__(self): app.add_settable(Settable('always_prefix_settables', bool, 'Prefix settables', app)) app._settables['str_value'] = Settable('str_value', str, 'String value', app) - assert 'arbitrary_value' in app.settables.keys() - assert 'always_prefix_settables' in app.settables.keys() - assert 'str_value' in app.settables.keys() + assert 'arbitrary_value' in app.settables + assert 'always_prefix_settables' in app.settables + assert 'str_value' in app.settables # verify the settable shows up out, err = run_cmd(app, 'set') - any(['arbitrary_value' in line and '5' in line for line in out]) + any('arbitrary_value' in line and '5' in line for line in out) out, err = run_cmd(app, 'set arbitrary_value') - any(['arbitrary_value' in line and '5' in line for line in out]) + any('arbitrary_value' in line and '5' in line for line in out) # change the value and verify the value changed out, err = run_cmd(app, 'set arbitrary_value 10') @@ -1115,7 +1106,7 @@ def __init__(self): """ assert out == normalize(expected) out, err = run_cmd(app, 'set arbitrary_value') - any(['arbitrary_value' in line and '10' in line for line in out]) + any('arbitrary_value' in line and '10' in line for line in out) # can't add to cmd2 now because commandset already has this settable with pytest.raises(KeyError): @@ -1152,7 +1143,10 @@ def __init__(self): cmdset_nopfx = WithSettablesNoPrefix() app.register_command_set(cmdset_nopfx) - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="Cannot force settable prefixes. CommandSet WithSettablesNoPrefix does not have a settable prefix defined.", + ): app.always_prefix_settables = True app.unregister_command_set(cmdset_nopfx) @@ -1166,18 +1160,18 @@ def __init__(self): app.register_command_set(cmdset) # Verify the settable is back with the defined prefix. - assert 'addon.arbitrary_value' in app.settables.keys() + assert 'addon.arbitrary_value' in app.settables # rename the prefix and verify that the prefix changes everywhere cmdset._settable_prefix = 'some' - assert 'addon.arbitrary_value' not in app.settables.keys() - assert 'some.arbitrary_value' in app.settables.keys() + assert 'addon.arbitrary_value' not in app.settables + assert 'some.arbitrary_value' in app.settables out, err = run_cmd(app, 'set') - any(['some.arbitrary_value' in line and '5' in line for line in out]) + any('some.arbitrary_value' in line and '5' in line for line in out) out, err = run_cmd(app, 'set some.arbitrary_value') - any(['some.arbitrary_value' in line and '5' in line for line in out]) + any('some.arbitrary_value' in line and '5' in line for line in out) # verify registering a commandset with duplicate prefix and settable names fails with pytest.raises(CommandSetRegistrationError): @@ -1201,9 +1195,9 @@ def __init__(self): class NsProviderSet(cmd2.CommandSet): # CommandSet which implements a namespace provider - def __init__(self, dummy): + def __init__(self, dummy) -> None: # Use dummy argument so this won't be autoloaded by other tests - super(NsProviderSet, self).__init__() + super().__init__() def ns_provider(self) -> argparse.Namespace: ns = argparse.Namespace() @@ -1216,7 +1210,7 @@ class NsProviderApp(cmd2.Cmd): # Used to test namespace providers in CommandSets def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - super(NsProviderApp, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @cmd2.with_argparser(cmd2.Cmd2ArgumentParser(), ns_provider=NsProviderSet.ns_provider) def do_test_ns(self, args: argparse.Namespace) -> None: @@ -1224,7 +1218,7 @@ def do_test_ns(self, args: argparse.Namespace) -> None: self.last_result = args.self -def test_ns_provider(): +def test_ns_provider() -> None: """This exercises code in with_argparser() decorator that calls namespace providers""" ns_provider_set = NsProviderSet(1) app = NsProviderApp(auto_load_commands=False)