diff --git a/CHANGELOG.md b/CHANGELOG.md index 8121a551..5071886b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.2 (TBD) + +- TBD + ## 2.6.1 (June 8, 2025) - Bug Fixes diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 09962e79..a4e040e5 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -1,5 +1,7 @@ """Import certain things for backwards compatibility.""" +from __future__ import annotations + import argparse import contextlib import importlib.metadata as importlib_metadata diff --git a/cmd2/ansi.py b/cmd2/ansi.py index cca02018..7cbf40dc 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -3,6 +3,8 @@ These are used for things like applying style to text, setting the window title, and asynchronous alerts. """ +from __future__ import annotations + import functools import re from enum import ( @@ -11,7 +13,6 @@ from typing import ( IO, Any, - Optional, cast, ) @@ -951,14 +952,14 @@ def __str__(self) -> str: def style( value: Any, *, - fg: Optional[FgColor] = None, - bg: Optional[BgColor] = None, - bold: Optional[bool] = None, - dim: Optional[bool] = None, - italic: Optional[bool] = None, - overline: Optional[bool] = None, - strikethrough: Optional[bool] = None, - underline: Optional[bool] = None, + fg: FgColor | None = None, + bg: BgColor | None = None, + bold: bool | None = None, + dim: bool | None = None, + italic: bool | None = None, + overline: bool | None = None, + strikethrough: bool | None = None, + underline: bool | None = None, ) -> str: """Apply ANSI colors and/or styles to a string and return it. diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 44f64ee1..04d574d9 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -3,6 +3,8 @@ See the header of argparse_custom.py for instructions on how to use these features. """ +from __future__ import annotations + import argparse import inspect import numbers @@ -11,7 +13,6 @@ ) from typing import ( TYPE_CHECKING, - Optional, Union, cast, ) @@ -28,6 +29,9 @@ from .cmd2 import ( Cmd, ) + from .command_definition import ( + CommandSet, + ) from .argparse_custom import ( ChoicesCallable, @@ -35,9 +39,6 @@ CompletionItem, generate_range_error, ) -from .command_definition import ( - CommandSet, -) from .exceptions import ( CompletionError, ) @@ -104,8 +105,8 @@ class _ArgumentState: def __init__(self, arg_action: argparse.Action) -> None: self.action = arg_action - self.min: Union[int, str] - self.max: Union[float, int, str] + self.min: int | str + self.max: float | int | str self.count = 0 self.is_remainder = self.action.nargs == argparse.REMAINDER @@ -162,7 +163,7 @@ class ArgparseCompleter: """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: dict[str, list[str]] | None = None ) -> None: """Create an ArgparseCompleter. @@ -202,7 +203,7 @@ 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 + self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: CommandSet | None = None ) -> list[str]: """Complete text using argparse metadata. @@ -227,10 +228,10 @@ def complete( skip_remaining_flags = False # _ArgumentState of the current positional - pos_arg_state: Optional[_ArgumentState] = None + pos_arg_state: _ArgumentState | None = None # _ArgumentState of the current flag - flag_arg_state: Optional[_ArgumentState] = None + flag_arg_state: _ArgumentState | None = None # Non-reusable flags that we've parsed matched_flags: list[str] = [] @@ -522,7 +523,7 @@ 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]: + def _format_completions(self, arg_state: _ArgumentState, completions: 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): @@ -652,7 +653,7 @@ def _complete_arg( arg_state: _ArgumentState, consumed_arg_values: dict[str, list[str]], *, - cmd_set: Optional[CommandSet] = None, + cmd_set: CommandSet | None = None, ) -> list[str]: """Tab completion routine for an argparse argument. @@ -660,7 +661,7 @@ def _complete_arg( :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: list[str] | ChoicesCallable if arg_state.action.choices is not None: arg_choices = list(arg_state.action.choices) if not arg_choices: diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index fd103685..ebeed671 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -221,6 +221,8 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser` for more details. """ +from __future__ import annotations + import argparse import re import sys @@ -229,7 +231,6 @@ 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, ) @@ -245,12 +246,16 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) runtime_checkable, ) +from typing_extensions import Self + from . import ( ansi, constants, ) if TYPE_CHECKING: # pragma: no cover + from collections.abc import Callable, Iterable, Sequence + from .argparse_completer import ( ArgparseCompleter, ) @@ -281,7 +286,7 @@ class CompletionItem(str): # noqa: SLOT000 See header of this file for more information """ - def __new__(cls, value: object, *_args: Any, **_kwargs: Any) -> 'CompletionItem': + def __new__(cls, value: object, *_args: Any, **_kwargs: Any) -> Self: """Responsible for creating and returning a new instance, called before __init__ when an object is instantiated.""" return super().__new__(cls, value) @@ -371,7 +376,7 @@ class ChoicesCallable: def __init__( self, is_completer: bool, - to_call: Union[CompleterFunc, ChoicesProviderFunc], + to_call: CompleterFunc | ChoicesProviderFunc, ) -> None: """Initialize the ChoiceCallable instance. @@ -432,7 +437,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]: +def _action_get_choices_callable(self: argparse.Action) -> ChoicesCallable | None: """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. @@ -518,7 +523,7 @@ def _action_set_completer( ############################################################################################################ # Patch argparse.Action with accessors for descriptive_header attribute ############################################################################################################ -def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]: +def _action_get_descriptive_header(self: argparse.Action) -> str | None: """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. @@ -534,7 +539,7 @@ def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]: setattr(argparse.Action, 'get_descriptive_header', _action_get_descriptive_header) -def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Optional[str]) -> None: +def _action_set_descriptive_header(self: argparse.Action, descriptive_header: str | None) -> None: """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. @@ -553,7 +558,7 @@ 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]]]: +def _action_get_nargs_range(self: argparse.Action) -> tuple[int, int | float] | None: """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. @@ -569,7 +574,7 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[ 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: +def _action_set_nargs_range(self: argparse.Action, nargs_range: tuple[int, int | float] | None) -> 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. @@ -628,7 +633,7 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool _CUSTOM_ATTRIB_PFX = '_attr_' -def register_argparse_argument_parameter(param_name: str, param_type: Optional[type[Any]]) -> None: +def register_argparse_argument_parameter(param_name: str, param_type: type[Any] | None) -> None: """Register a custom argparse argument parameter. The registered name will then be a recognized keyword parameter to the parser's `add_argument()` function. @@ -694,11 +699,11 @@ 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, - choices_provider: Optional[ChoicesProviderFunc] = None, - completer: Optional[CompleterFunc] = None, + nargs: int | str | tuple[int] | tuple[int, int] | tuple[int, float] | None = None, + choices_provider: ChoicesProviderFunc | None = None, + completer: CompleterFunc | None = None, suppress_tab_hint: bool = False, - descriptive_header: Optional[str] = None, + descriptive_header: str | None = None, **kwargs: Any, ) -> argparse.Action: """Wrap ActionsContainer.add_argument() which supports more settings used by cmd2. @@ -744,7 +749,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: 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 @@ -885,7 +890,7 @@ 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']]: # noqa: N802 +def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> type[ArgparseCompleter] | None: # 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. @@ -901,7 +906,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Opti 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: # noqa: N802 +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. @@ -996,10 +1001,10 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter): def _format_usage( self, - usage: Optional[str], + usage: str | None, actions: Iterable[argparse.Action], groups: Iterable[argparse._ArgumentGroup], - prefix: Optional[str] = None, + prefix: str | None = None, ) -> str: if prefix is None: prefix = gettext('Usage: ') @@ -1053,7 +1058,7 @@ def _format_usage( # End cmd2 customization # helper for wrapping lines - def get_lines(parts: list[str], indent: str, prefix: Optional[str] = None) -> list[str]: + def get_lines(parts: list[str], indent: str, prefix: str | None = None) -> list[str]: lines: list[str] = [] line: list[str] = [] line_len = len(prefix) - 1 if prefix is not None else len(indent) - 1 @@ -1133,8 +1138,8 @@ def _format_action_invocation(self, action: argparse.Action) -> str: def _determine_metavar( self, action: argparse.Action, - default_metavar: Union[str, tuple[str, ...]], - ) -> Union[str, tuple[str, ...]]: + default_metavar: str | tuple[str, ...], + ) -> str | tuple[str, ...]: """Determine what to use as the metavar value of an action.""" if action.metavar is not None: result = action.metavar @@ -1150,7 +1155,7 @@ def _determine_metavar( def _metavar_formatter( self, action: argparse.Action, - default_metavar: Union[str, tuple[str, ...]], + default_metavar: str | tuple[str, ...], ) -> Callable[[int], tuple[str, ...]]: metavar = self._determine_metavar(action, default_metavar) @@ -1161,7 +1166,7 @@ def format_tuple(tuple_size: int) -> tuple[str, ...]: return format_tuple - def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str: + def _format_args(self, action: argparse.Action, default_metavar: str | tuple[str, ...]) -> str: """Handle ranged nargs and make other output less verbose.""" metavar = self._determine_metavar(action, default_metavar) metavar_formatter = self._metavar_formatter(action, default_metavar) @@ -1191,15 +1196,15 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): def __init__( self, - prog: Optional[str] = None, - usage: Optional[str] = None, - description: Optional[str] = None, - epilog: Optional[str] = None, + prog: str | None = None, + usage: str | None = None, + description: str | None = None, + epilog: str | None = None, parents: Sequence[argparse.ArgumentParser] = (), formatter_class: type[argparse.HelpFormatter] = Cmd2HelpFormatter, prefix_chars: str = '-', - fromfile_prefix_chars: Optional[str] = None, - argument_default: Optional[str] = None, + fromfile_prefix_chars: str | None = None, + argument_default: str | None = None, conflict_handler: str = 'error', add_help: bool = True, allow_abbrev: bool = True, @@ -1207,7 +1212,7 @@ def __init__( suggest_on_error: bool = False, color: bool = False, *, - ap_completer_type: Optional[type['ArgparseCompleter']] = None, + ap_completer_type: type[ArgparseCompleter] | None = None, ) -> None: """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2. @@ -1341,7 +1346,7 @@ def format_help(self) -> str: # determine help from format above return formatter.format_help() + '\n' - def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: # type: ignore[override] + def _print_message(self, message: str, file: IO[str] | None = None) -> None: # type: ignore[override] # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color if message: if file is None: diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 284d57df..18e56e92 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -1,5 +1,7 @@ """Module provides basic ability to copy from and paste to the clipboard/pastebuffer.""" +from __future__ import annotations + import typing import pyperclip # type: ignore[import] diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index bc282e57..c5d67404 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -28,6 +28,8 @@ # For example, we don't import the 'traceback' module # until the pexcept() function is called and the debug # setting is True +from __future__ import annotations + import argparse import cmd import contextlib @@ -50,10 +52,6 @@ namedtuple, ) from collections.abc import Callable, Iterable, Mapping -from types import ( - FrameType, - ModuleType, -) from typing import ( IO, TYPE_CHECKING, @@ -149,6 +147,12 @@ suggest_similar, ) +if TYPE_CHECKING: # pragma: no cover + from types import ( + FrameType, + ModuleType, + ) + # Set up readline if rl_type == RlType.NONE: # pragma: no cover sys.stderr.write(ansi.style_warning(rl_warning)) @@ -183,7 +187,7 @@ class _SavedReadlineSettings: def __init__(self) -> None: self.completer = None self.delims = '' - self.basic_quotes: Optional[bytes] = None + self.basic_quotes: bytes | None = None class _SavedCmd2Env: @@ -191,10 +195,10 @@ class _SavedCmd2Env: def __init__(self) -> None: self.readline_settings = _SavedReadlineSettings() - self.readline_module: Optional[ModuleType] = None + self.readline_module: ModuleType | None = None self.history: list[str] = [] - self.sys_stdout: Optional[TextIO] = None - self.sys_stdin: Optional[TextIO] = None + self.sys_stdout: TextIO | None = None + self.sys_stdin: TextIO | None = None # Contains data about a disabled command which is used to restore its original functions when the command is enabled @@ -215,7 +219,7 @@ class _CommandParsers: Parser creation and retrieval are accomplished through the get() method. """ - def __init__(self, cmd: 'Cmd') -> None: + def __init__(self, cmd: Cmd) -> None: self._cmd = cmd # Keyed by the fully qualified method names. This is more reliable than @@ -239,7 +243,7 @@ def __contains__(self, command_method: CommandFunc) -> bool: parser = self.get(command_method) return bool(parser) - def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]: + def get(self, command_method: CommandFunc) -> argparse.ArgumentParser | None: """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. @@ -306,8 +310,8 @@ class Cmd(cmd.Cmd): def __init__( self, completekey: str = 'tab', - stdin: Optional[TextIO] = None, - stdout: Optional[TextIO] = None, + stdin: TextIO | None = None, + stdout: TextIO | None = None, *, persistent_history_file: str = '', persistent_history_length: int = 1000, @@ -316,12 +320,12 @@ def __init__( include_py: bool = False, include_ipy: bool = False, allow_cli_args: bool = True, - transcript_files: Optional[list[str]] = None, + transcript_files: list[str] | None = None, allow_redirection: bool = True, - multiline_commands: Optional[list[str]] = None, - terminators: Optional[list[str]] = None, - shortcuts: Optional[dict[str, str]] = None, - command_sets: Optional[Iterable[CommandSet]] = None, + multiline_commands: list[str] | None = None, + terminators: list[str] | None = None, + shortcuts: dict[str, str] | None = None, + command_sets: Iterable[CommandSet] | None = None, auto_load_commands: bool = True, allow_clipboard: bool = True, suggest_similar_command: bool = False, @@ -465,7 +469,7 @@ def __init__( # If the current command created a process to pipe to, then this will be a ProcReader object. # Otherwise it will be None. It's used to know when a pipe process can be killed and/or waited upon. - self._cur_pipe_proc_reader: Optional[utils.ProcReader] = None + self._cur_pipe_proc_reader: utils.ProcReader | None = None # Used to keep track of whether we are redirecting or piping output self._redirecting = False @@ -501,7 +505,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: list[str] | None = None # Check for command line args if allow_cli_args: @@ -630,7 +634,7 @@ def __init__( self.default_suggestion_message = "Did you mean {}?" # the current command being executed - self.current_command: Optional[Statement] = None + self.current_command: Statement | None = None def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]: """Find all CommandSets that match the provided CommandSet type. @@ -647,7 +651,7 @@ def find_commandsets(self, commandset_type: type[CommandSet], *, subclass_match: if type(cmdset) == commandset_type or (subclass_match and isinstance(cmdset, commandset_type)) # noqa: E721 ] - def find_commandset_for_command(self, command_name: str) -> Optional[CommandSet]: + def find_commandset_for_command(self, command_name: str) -> CommandSet | None: """Find the CommandSet that registered the command name. :param command_name: command name to search @@ -758,16 +762,13 @@ def register_command_set(self, cmdset: CommandSet) -> None: def _build_parser( self, parent: CommandParent, - parser_builder: Optional[ - Union[ - argparse.ArgumentParser, - Callable[[], argparse.ArgumentParser], - StaticArgParseBuilder, - ClassArgParseBuilder, - ] - ], - ) -> Optional[argparse.ArgumentParser]: - parser: Optional[argparse.ArgumentParser] = None + parser_builder: argparse.ArgumentParser + | Callable[[], argparse.ArgumentParser] + | StaticArgParseBuilder + | ClassArgParseBuilder + | None, + ) -> argparse.ArgumentParser | None: + parser: argparse.ArgumentParser | None = None if isinstance(parser_builder, staticmethod): parser = parser_builder.__func__() elif isinstance(parser_builder, classmethod): @@ -904,7 +905,7 @@ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None: if command_parser is not None: check_parser_uninstallable(command_parser) - def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: + def _register_subcommands(self, cmdset: CommandSet | Cmd) -> None: """Register subcommands with their base command. :param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands @@ -1010,7 +1011,7 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset) break - def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: + def _unregister_subcommands(self, cmdset: CommandSet | Cmd) -> None: """Unregister subcommands from their base command. :param cmdset: CommandSet containing subcommands @@ -1190,7 +1191,7 @@ def print_to( msg: Any, *, end: str = '\n', - style: Optional[Callable[[str], str]] = None, + style: Callable[[str], str] | None = None, ) -> None: """Print message to a given file object. @@ -1340,7 +1341,7 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: else: self.poutput(msg, end=end) - def ppretty(self, data: Any, *, indent: int = 2, width: int = 80, depth: Optional[int] = None, end: str = '\n') -> None: + def ppretty(self, data: Any, *, indent: int = 2, width: int = 80, depth: int | None = None, end: str = '\n') -> None: """Pretty print arbitrary Python data structures to self.stdout and appends a newline by default. :param data: object to print @@ -1531,9 +1532,9 @@ def flag_based_complete( line: str, begidx: int, endidx: int, - flag_dict: dict[str, Union[Iterable[str], CompleterFunc]], + flag_dict: dict[str, Iterable[str] | CompleterFunc], *, - all_else: Union[None, Iterable[str], CompleterFunc] = None, + all_else: None | Iterable[str] | CompleterFunc = None, ) -> list[str]: """Tab completes based on a particular flag preceding the token being completed. @@ -1580,9 +1581,9 @@ def index_based_complete( line: str, begidx: int, endidx: int, - index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]], + index_dict: Mapping[int, Iterable[str] | CompleterFunc], *, - all_else: Optional[Union[Iterable[str], CompleterFunc]] = None, + all_else: Iterable[str] | CompleterFunc | None = None, ) -> list[str]: """Tab completes based on a fixed position in the input string. @@ -1610,7 +1611,7 @@ def index_based_complete( index = len(tokens) - 1 # Check if token is at an index in the dictionary - match_against: Optional[Union[Iterable[str], CompleterFunc]] + match_against: Iterable[str] | CompleterFunc | None match_against = index_dict.get(index, all_else) # Perform tab completion using a Iterable @@ -1630,7 +1631,7 @@ def path_complete( begidx: int, # noqa: ARG002 endidx: int, *, - path_filter: Optional[Callable[[str], bool]] = None, + path_filter: Callable[[str], bool] | None = None, ) -> list[str]: """Perform completion of local file system paths. @@ -2006,7 +2007,7 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar return completer_type def _perform_completion( - self, text: str, line: str, begidx: int, endidx: int, custom_settings: Optional[utils.CustomCompletionSettings] = None + self, text: str, line: str, begidx: int, endidx: int, custom_settings: utils.CustomCompletionSettings | None = None ) -> None: """Perform the actual completion, helper function for complete(). @@ -2184,8 +2185,8 @@ def _perform_completion( self.completion_matches[0] += completion_token_quote def complete( # type: ignore[override] - self, text: str, state: int, custom_settings: Optional[utils.CustomCompletionSettings] = None - ) -> Optional[str]: + self, text: str, state: int, custom_settings: utils.CustomCompletionSettings | None = None + ) -> str | None: """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, …, @@ -2377,7 +2378,7 @@ def get_help_topics(self) -> list[str]: # Filter out hidden and disabled commands return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands] - def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: # noqa: ARG002 + def sigint_handler(self, signum: int, _: FrameType | None) -> None: # noqa: ARG002 """Signal handler for SIGINTs which typically come from Ctrl-C events. If you need custom SIGINT behavior, then override this method. @@ -2399,7 +2400,7 @@ def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: # noqa: if raise_interrupt: self._raise_keyboard_interrupt() - def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None: + def termination_signal_handler(self, signum: int, _: FrameType | None) -> None: """Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac. SIGHUP - received when terminal window is closed @@ -2419,7 +2420,7 @@ def _raise_keyboard_interrupt(self) -> None: """Raise a KeyboardInterrupt.""" raise KeyboardInterrupt("Got a keyboard interrupt") - def precmd(self, statement: Union[Statement, str]) -> Statement: + def precmd(self, statement: Statement | str) -> Statement: """Ran just before the command is executed by [cmd2.Cmd.onecmd][] and after adding it to history (cmd Hook method). :param statement: subclass of str which also contains the parsed input @@ -2430,7 +2431,7 @@ def precmd(self, statement: Union[Statement, str]) -> Statement: """ return Statement(statement) if not isinstance(statement, Statement) else statement - def postcmd(self, stop: bool, statement: Union[Statement, str]) -> bool: # noqa: ARG002 + def postcmd(self, stop: bool, statement: Statement | str) -> bool: # noqa: ARG002 """Ran just after a command is executed by [cmd2.Cmd.onecmd][] (cmd inherited Hook method). :param stop: return `True` to request the command loop terminate @@ -2479,7 +2480,7 @@ def onecmd_plus_hooks( add_to_history: bool = True, raise_keyboard_interrupt: bool = False, py_bridge_call: bool = False, - orig_rl_history_length: Optional[int] = None, + orig_rl_history_length: int | None = None, ) -> bool: """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks. @@ -2520,7 +2521,7 @@ def onecmd_plus_hooks( # we need to run the finalization hooks raise EmptyStatement # noqa: TRY301 - redir_saved_state: Optional[utils.RedirectionSavedState] = None + redir_saved_state: utils.RedirectionSavedState | None = None try: # Get sigint protection while we set up redirection @@ -2602,7 +2603,7 @@ def onecmd_plus_hooks( return stop - def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) -> bool: + def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> bool: """Run the command finalization hooks.""" with self.sigint_protection: if not sys.platform.startswith('win') and self.stdin.isatty(): @@ -2622,7 +2623,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement]) def runcmds_plus_hooks( self, - cmds: Union[list[HistoryItem], list[str]], + cmds: list[HistoryItem] | list[str], *, add_to_history: bool = True, stop_on_keyboard_interrupt: bool = False, @@ -2657,7 +2658,7 @@ def runcmds_plus_hooks( return False - def _complete_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement: + def _complete_statement(self, line: str, *, orig_rl_history_length: int | None = None) -> Statement: """Keep accepting lines of input until the command is complete. There is some pretty hacky code here to handle some quirks of @@ -2747,7 +2748,7 @@ def combine_rl_history(statement: Statement) -> None: return statement - def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement: + def _input_line_to_statement(self, line: str, *, orig_rl_history_length: int | None = None) -> Statement: """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 @@ -2800,7 +2801,7 @@ def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optiona ) return statement - def _resolve_macro(self, statement: Statement) -> Optional[str]: + def _resolve_macro(self, statement: Statement) -> str | None: """Resolve a macro and return the resulting string. :param statement: the parsed statement from the command line @@ -2855,7 +2856,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState: ) # The ProcReader for this command - cmd_pipe_proc_reader: Optional[utils.ProcReader] = None + cmd_pipe_proc_reader: utils.ProcReader | None = None if not self.allow_redirection: # Don't return since we set some state variables at the end of the function @@ -2979,7 +2980,7 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec self._cur_pipe_proc_reader = saved_redir_state.saved_pipe_proc_reader self._redirecting = saved_redir_state.saved_redirecting - def cmd_func(self, command: str) -> Optional[CommandFunc]: + def cmd_func(self, command: str) -> CommandFunc | None: """Get the function for a command. :param command: the name of the command @@ -2996,7 +2997,7 @@ def cmd_func(self, command: str) -> Optional[CommandFunc]: func = getattr(self, func_name, None) return cast(CommandFunc, func) if callable(func) else None - def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool: + def onecmd(self, statement: Statement | str, *, add_to_history: bool = True) -> bool: """Execute the actual do_* method for a command. If the command provided doesn't exist, then it executes default() instead. @@ -3031,7 +3032,7 @@ def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = Tru return stop if stop is not None else False - def default(self, statement: Statement) -> Optional[bool]: # type: ignore[override] + def default(self, statement: Statement) -> bool | None: # type: ignore[override] """Execute when the command given isn't a recognized command implemented by a do_* method. :param statement: Statement object with parsed input @@ -3049,20 +3050,20 @@ def default(self, statement: Statement) -> Optional[bool]: # type: ignore[overr self.perror(err_msg, apply_style=False) return None - def _suggest_similar_command(self, command: str) -> Optional[str]: + def _suggest_similar_command(self, command: str) -> str | None: return suggest_similar(command, self.get_visible_commands()) def read_input( self, prompt: str, *, - history: Optional[list[str]] = None, + history: list[str] | None = None, completion_mode: utils.CompletionMode = utils.CompletionMode.NONE, preserve_quotes: bool = False, - choices: Optional[Iterable[Any]] = None, - choices_provider: Optional[ChoicesProviderFunc] = None, - completer: Optional[CompleterFunc] = None, - parser: Optional[argparse.ArgumentParser] = None, + choices: Iterable[Any] | None = None, + choices_provider: ChoicesProviderFunc | None = None, + completer: CompleterFunc | None = None, + parser: argparse.ArgumentParser | None = None, ) -> str: """Read input from appropriate stdin value. @@ -3096,8 +3097,8 @@ def read_input( :raises Exception: any exceptions raised by input() and stdin.readline() """ readline_configured = False - saved_completer: Optional[CompleterFunc] = None - saved_history: Optional[list[str]] = None + saved_completer: CompleterFunc | None = None + saved_history: list[str] | None = None def configure_readline() -> None: """Configure readline tab completion and history.""" @@ -3116,7 +3117,7 @@ def configure_readline() -> None: # Disable completion if completion_mode == utils.CompletionMode.NONE: - def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover # noqa: ARG001 + def complete_none(text: str, state: int) -> str | None: # pragma: no cover # noqa: ARG001 return None complete_func = complete_none @@ -3802,7 +3803,7 @@ 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: # noqa: ARG002 + def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: int) -> None: # noqa: ARG002 """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. @@ -3820,7 +3821,7 @@ 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: list[str] | None, 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. @@ -3957,7 +3958,7 @@ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None: if (cmd_func := self.cmd_func(command)) is None: continue - doc: Optional[str] + doc: str | None # If this is an argparse command, use its description. if (cmd_parser := self._command_parsers.get(cmd_func)) is not None: @@ -4009,7 +4010,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: ) @with_argparser(eof_parser) - def do_eof(self, _: argparse.Namespace) -> Optional[bool]: + def do_eof(self, _: argparse.Namespace) -> bool | None: """Quit with no arguments, called when Ctrl-D is pressed. This can be overridden if quit should be called differently. @@ -4022,13 +4023,13 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]: quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application") @with_argparser(quit_parser) - def do_quit(self, _: argparse.Namespace) -> Optional[bool]: + def do_quit(self, _: argparse.Namespace) -> bool | None: """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: str | list[str] | list[tuple[Any, str | None]], prompt: str = 'Your choice? ') -> Any: """Present a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. @@ -4041,12 +4042,12 @@ def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], p that the return value can differ from the text advertised to the user """ - local_opts: Union[list[str], list[tuple[Any, Optional[str]]]] + local_opts: list[str] | list[tuple[Any, str | None]] if isinstance(opts, str): 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, str | None]] = [] for opt in local_opts: if isinstance(opt, str): fulloptions.append((opt, opt)) @@ -4376,7 +4377,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None: else: sys.modules['readline'] = cmd2_env.readline_module - def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]: + def _run_python(self, *, pyscript: str | None = None) -> bool | None: """Run an interactive Python shell or execute a pyscript file. Called by do_py() and do_run_pyscript(). @@ -4497,7 +4498,7 @@ def py_quit() -> None: py_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell") @with_argparser(py_parser) - def do_py(self, _: argparse.Namespace) -> Optional[bool]: + def do_py(self, _: argparse.Namespace) -> bool | None: """Run an interactive Python shell. :return: True if running of commands should stop. @@ -4512,7 +4513,7 @@ def do_py(self, _: argparse.Namespace) -> Optional[bool]: ) @with_argparser(run_pyscript_parser) - def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: + def do_run_pyscript(self, args: argparse.Namespace) -> bool | None: """Run a Python script file inside the console. :return: True if running of commands should stop @@ -4548,7 +4549,7 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell") @with_argparser(ipython_parser) - def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover + def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover """Enter an interactive IPython shell. :return: True if running of commands should stop @@ -4667,7 +4668,7 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help) @with_argparser(history_parser) - def do_history(self, args: argparse.Namespace) -> Optional[bool]: + def do_history(self, args: argparse.Namespace) -> bool | None: """View, run, edit, save, or clear previously entered commands. :return: True if running of commands should stop @@ -4759,7 +4760,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]: self.last_result = history return None - def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]': + def _get_history(self, args: argparse.Namespace) -> OrderedDict[int, HistoryItem]: """If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history. This function returns a dictionary with history items keyed by their 1-based index in ascending order. @@ -4898,7 +4899,7 @@ def _persist_history(self) -> None: def _generate_transcript( self, - history: Union[list[HistoryItem], list[str]], + history: list[HistoryItem] | list[str], transcript_file: str, *, add_to_history: bool = True, @@ -5010,7 +5011,7 @@ def do_edit(self, args: argparse.Namespace) -> None: # 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: + def run_editor(self, file_path: str | None = None) -> None: """Run a text editor and optionally open a file with it. :param file_path: optional path of the file to edit. Defaults to None. @@ -5026,7 +5027,7 @@ def run_editor(self, file_path: Optional[str] = None) -> None: self.do_shell(command) @property - def _current_script_dir(self) -> Optional[str]: + def _current_script_dir(self) -> str | None: """Accessor to get the current script directory from the _script_dir LIFO queue.""" if self._script_dir: return self._script_dir[-1] @@ -5053,7 +5054,7 @@ def _current_script_dir(self) -> Optional[str]: run_script_parser.add_argument('script_path', help="path to the script file", completer=path_complete) @with_argparser(run_script_parser) - def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: + def do_run_script(self, args: argparse.Namespace) -> bool | None: """Run commands in script file that is encoded as either ASCII or UTF-8 text. :return: True if running of commands should stop @@ -5130,7 +5131,7 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script') @with_argparser(relative_run_script_parser) - def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]: + def do__relative_run_script(self, args: argparse.Namespace) -> bool | None: """Run commands in script file that is encoded as either ASCII or UTF-8 text. :return: True if running of commands should stop @@ -5204,7 +5205,7 @@ class TestMyAppCase(Cmd2TestCase): # Return a failure error code to support automated transcript-based testing self.exit_code = 1 - def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover + def async_alert(self, alert_msg: str, new_prompt: str | None = None) -> None: # pragma: no cover """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 @@ -5447,7 +5448,7 @@ def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_ # Set apply_style to False so message_to_print's style is not overridden self.perror(message_to_print, apply_style=False) - def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] + def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override] """Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop(). _cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with @@ -5638,8 +5639,8 @@ def register_cmdfinalization_hook( def _resolve_func_self( self, cmd_support_func: Callable[..., Any], - cmd_self: Union[CommandSet, 'Cmd', None], - ) -> Optional[object]: + cmd_self: CommandSet | Cmd | None, + ) -> object | None: """Attempt to resolve a candidate instance to pass as 'self'. Used for an unbound class method that was used when defining command's argparse object. @@ -5651,7 +5652,7 @@ def _resolve_func_self( :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: type[Any] | None = 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): @@ -5662,7 +5663,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: CommandSet | Cmd | None # check if the command's CommandSet is a sub-class of the support function's defining class if isinstance(cmd_self, func_class): diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 860fd5d1..fa5ae34b 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -1,5 +1,7 @@ """Supports the definition of commands in separate classes to be composed into cmd2.Cmd.""" +from __future__ import annotations + from collections.abc import Callable, Mapping from typing import ( TYPE_CHECKING, @@ -14,13 +16,14 @@ from .exceptions import ( CommandSetRegistrationError, ) -from .utils import ( - Settable, -) if TYPE_CHECKING: # pragma: no cover import cmd2 + from .utils import ( + Settable, + ) + #: Callable signature for a basic command function #: Further refinements are needed to define the input parameters CommandFunc = Callable[..., Optional[bool]] @@ -91,13 +94,13 @@ def __init__(self) -> None: This will be set when the CommandSet is registered and it should be accessed by child classes using the self._cmd property. """ - self.__cmd_internal: Optional[cmd2.Cmd] = None + self.__cmd_internal: cmd2.Cmd | None = None self._settables: dict[str, Settable] = {} self._settable_prefix = self.__class__.__name__ @property - def _cmd(self) -> 'cmd2.Cmd': + def _cmd(self) -> cmd2.Cmd: """Property for child classes to access self.__cmd_internal. Using this property ensures that self.__cmd_internal has been set @@ -112,7 +115,7 @@ def _cmd(self) -> 'cmd2.Cmd': raise CommandSetRegistrationError('This CommandSet is not registered') return self.__cmd_internal - def on_register(self, cmd: 'cmd2.Cmd') -> None: + def on_register(self, cmd: cmd2.Cmd) -> None: """First step to registering a CommandSet, called by cmd2.Cmd. The commands defined in this class have not been added to the CLI object at this point. diff --git a/cmd2/constants.py b/cmd2/constants.py index c82b3ca1..ada19f6f 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -2,6 +2,7 @@ # Unless documented in https://cmd2.readthedocs.io/en/latest/api/index.html # nothing here should be considered part of the public API of this module +from __future__ import annotations INFINITY = float('inf') diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 61742ad3..864576c6 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -1,5 +1,7 @@ """Decorators for ``cmd2`` commands.""" +from __future__ import annotations + import argparse from collections.abc import Callable, Sequence from typing import ( @@ -73,7 +75,7 @@ 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]]: +def _parse_positionals(args: tuple[Any, ...]) -> tuple[cmd2.Cmd, Statement | str]: """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. @@ -98,7 +100,7 @@ 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]: +def _arg_swap(args: Sequence[Any], search_arg: Any, *replace_arg: Any) -> list[Any]: """Swap the Statement parameter with one or more decorator-specific parameters. :param args: The original positional arguments @@ -131,13 +133,13 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> def with_argument_list( - func_arg: Optional[ArgListCommandFunc[CommandParent]] = None, + func_arg: ArgListCommandFunc[CommandParent] | None = None, *, preserve_quotes: bool = False, -) -> Union[ - RawCommandFuncOptionalBoolReturn[CommandParent], - Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]], -]: +) -> ( + RawCommandFuncOptionalBoolReturn[CommandParent] + | Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]] +): """Decorate a ``do_*`` method to alter the arguments passed to it so it is passed a list[str]. Default passes a string of whatever the user typed. With this decorator, the @@ -169,7 +171,7 @@ def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOpti """ @functools.wraps(func) - def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: + def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: """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: @@ -269,13 +271,11 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: def with_argparser( - parser: Union[ - argparse.ArgumentParser, # existing parser - Callable[[], argparse.ArgumentParser], # function or staticmethod - Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod - ], + parser: argparse.ArgumentParser + | Callable[[], argparse.ArgumentParser] + | Callable[[CommandParentType], argparse.ArgumentParser], *, - ns_provider: Optional[Callable[..., argparse.Namespace]] = None, + ns_provider: Callable[..., argparse.Namespace] | None = None, preserve_quotes: bool = False, with_unknown_args: bool = False, ) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: @@ -336,7 +336,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOpt """ @functools.wraps(func) - def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: + def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> bool | None: """Command function wrapper which translates command line into argparse Namespace and call actual command function. :param args: All positional arguments to this function. We're expecting there to be: @@ -367,7 +367,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: tuple[argparse.Namespace] | tuple[argparse.Namespace, list[str]] if with_unknown_args: new_args = arg_parser.parse_known_args(parsed_arglist, namespace) else: @@ -405,14 +405,12 @@ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]: def as_subcommand_to( command: str, subcommand: str, - parser: Union[ - argparse.ArgumentParser, # existing parser - Callable[[], argparse.ArgumentParser], # function or staticmethod - Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod - ], + parser: argparse.ArgumentParser + | Callable[[], argparse.ArgumentParser] + | Callable[[CommandParentType], argparse.ArgumentParser], *, - help: Optional[str] = None, # noqa: A002 - aliases: Optional[list[str]] = None, + help: str | None = None, # noqa: A002 + aliases: list[str] | None = None, ) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: """Tag this method as a subcommand to an existing argparse decorated command. diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index 5d0cd190..9a76c129 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -1,5 +1,7 @@ """Custom exceptions for cmd2.""" +from __future__ import annotations + from typing import Any ############################################################################################################ diff --git a/cmd2/history.py b/cmd2/history.py index 1a8582b6..e98a84eb 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -1,18 +1,18 @@ """History management classes.""" +from __future__ import annotations + import json import re from collections import ( OrderedDict, ) -from collections.abc import Callable, Iterable from dataclasses import ( dataclass, ) from typing import ( + TYPE_CHECKING, Any, - Optional, - Union, overload, ) @@ -24,6 +24,9 @@ shlex_split, ) +if TYPE_CHECKING: # pragma: no cover + from collections.abc import Callable, Iterable + def single_line_format(statement: Statement) -> str: """Format a command line to display on a single line. @@ -126,7 +129,7 @@ def to_dict(self) -> dict[str, Any]: return {HistoryItem._statement_field: self.statement.to_dict()} @staticmethod - def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem': + def from_dict(source_dict: dict[str, Any]) -> HistoryItem: """Restore a HistoryItem from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) @@ -164,7 +167,7 @@ def start_session(self) -> None: """Start a new session, thereby setting the next index as the first index in the new session.""" self.session_start_index = len(self) - def _zero_based_index(self, onebased: Union[int, str]) -> int: + def _zero_based_index(self, onebased: int | str) -> int: """Convert a one-based index to a zero-based index.""" result = int(onebased) if result > 0: @@ -177,7 +180,7 @@ def append(self, new: HistoryItem) -> None: ... # pragma: no cover @overload def append(self, new: Statement) -> None: ... # pragma: no cover - def append(self, new: Union[Statement, HistoryItem]) -> None: + def append(self, new: Statement | HistoryItem) -> None: """Append a new statement to the end of the History list. :param new: Statement object which will be composed into a HistoryItem @@ -229,7 +232,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]': + def span(self, span: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Return a slice of the History list. :param span: string containing an index or a slice @@ -278,7 +281,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]': + def str_search(self, search: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Find history items which contain a given string. :param search: the string to search for @@ -297,7 +300,7 @@ def isin(history_item: HistoryItem) -> bool: start = 0 if include_persisted else self.session_start_index return self._build_result_dictionary(start, len(self), isin) - def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]': + def regex_search(self, regex: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]: """Find history items which match a given regular expression. :param regex: the regular expression to search for. @@ -332,8 +335,8 @@ def truncate(self, max_length: int) -> None: del self[0:last_element] def _build_result_dictionary( - self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None - ) -> 'OrderedDict[int, HistoryItem]': + self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None + ) -> OrderedDict[int, HistoryItem]: """Build history search results. :param start: start index to search from @@ -354,7 +357,7 @@ def to_json(self) -> str: return json.dumps(json_dict, ensure_ascii=False, indent=2) @staticmethod - def from_json(history_json: str) -> 'History': + def from_json(history_json: str) -> History: """Restore History from a JSON string. :param history_json: history data as JSON string (generated using to_json()) diff --git a/cmd2/parsing.py b/cmd2/parsing.py index e12f799c..69bae263 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -1,18 +1,20 @@ """Statement parsing classes for cmd2.""" +from __future__ import annotations + import re import shlex -from collections.abc import Iterable from dataclasses import ( dataclass, field, ) from typing import ( + TYPE_CHECKING, Any, - Optional, - Union, ) +from typing_extensions import Self + from . import ( constants, utils, @@ -21,6 +23,9 @@ Cmd2ShlexError, ) +if TYPE_CHECKING: # pragma: no cover + from collections.abc import Iterable + def shlex_split(str_to_split: str) -> list[str]: """Split the string *str_to_split* using shell-like syntax. @@ -149,7 +154,7 @@ class Statement(str): # type: ignore[override] # noqa: SLOT000 # Used in JSON dictionaries _args_field = 'args' - def __new__(cls, value: object, *_pos_args: Any, **_kw_args: Any) -> 'Statement': + def __new__(cls, value: object, *_pos_args: Any, **_kw_args: Any) -> Self: """Create a new instance of Statement. We must override __new__ because we are subclassing `str` which is @@ -225,7 +230,7 @@ def to_dict(self) -> dict[str, Any]: return self.__dict__.copy() @staticmethod - def from_dict(source_dict: dict[str, Any]) -> 'Statement': + def from_dict(source_dict: dict[str, Any]) -> Statement: """Restore a Statement from a dictionary. :param source_dict: source data dictionary (generated using to_dict()) @@ -250,10 +255,10 @@ class StatementParser: 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, + terminators: Iterable[str] | None = None, + multiline_commands: Iterable[str] | None = None, + aliases: dict[str, str] | None = None, + shortcuts: dict[str, str] | None = None, ) -> None: """Initialize an instance of StatementParser. @@ -585,7 +590,7 @@ def parse_command_only(self, rawinput: str) -> 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 + self, command_name: str, to_parse: Statement | str, preserve_quotes: bool ) -> tuple[Statement, list[str]]: """Retrieve just the arguments being passed to their ``do_*`` methods as a list. diff --git a/cmd2/plugin.py b/cmd2/plugin.py index 92cb80bd..a5a32873 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,13 +1,16 @@ """Classes for the cmd2 plugin system.""" +from __future__ import annotations + from dataclasses import ( dataclass, ) -from typing import Optional +from typing import TYPE_CHECKING -from .parsing import ( - Statement, -) +if TYPE_CHECKING: # pragma: no cover + from .parsing import ( + Statement, + ) @dataclass @@ -38,4 +41,4 @@ class CommandFinalizationData: """Data class containing information passed to command finalization hook methods.""" stop: bool - statement: Optional[Statement] + statement: Statement | None diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 2a147583..411d1413 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -3,6 +3,8 @@ Maintains a reasonable degree of isolation between the two. """ +from __future__ import annotations + import sys from contextlib import ( redirect_stderr, @@ -13,7 +15,6 @@ TYPE_CHECKING, Any, NamedTuple, - Optional, TextIO, Union, cast, @@ -86,7 +87,7 @@ class PyBridge: Defaults to True. """ - def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None: + def __init__(self, cmd2_app: cmd2.Cmd, *, add_to_history: bool = True) -> None: """Initialize PyBridge instances.""" self._cmd2_app = cmd2_app self._add_to_history = add_to_history @@ -101,7 +102,7 @@ def __dir__(self) -> list[str]: attributes.insert(0, 'cmd_echo') return attributes - def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResult: + def __call__(self, command: str, *, echo: bool | None = None) -> CommandResult: """Provide functionality to call application commands by calling PyBridge. ex: app('help') diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index a07479c7..ebf0e137 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -1,11 +1,12 @@ """Imports the proper Readline for the platform and provides utility functions for it.""" +from __future__ import annotations + import contextlib import sys from enum import ( Enum, ) -from typing import Union ######################################################################################################################### # NOTE ON LIBEDIT: @@ -191,7 +192,7 @@ def rl_get_prompt() -> str: # pragma: no cover 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 + prompt_data: str | bytes = readline.rl.prompt prompt = prompt_data.decode(encoding='utf-8') if isinstance(prompt_data, bytes) else prompt_data else: diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 35c89e10..6f7947f3 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -5,18 +5,19 @@ There are already implemented and ready-to-use examples of this below TableCreator's code. """ +from __future__ import annotations + import copy import io from collections import ( deque, ) -from collections.abc import Sequence from enum import ( Enum, ) from typing import ( + TYPE_CHECKING, Any, - Optional, ) from wcwidth import ( # type: ignore[import] @@ -29,6 +30,9 @@ utils, ) +if TYPE_CHECKING: # pragma: no cover + from collections.abc import Sequence + # Constants EMPTY = '' SPACE = ' ' @@ -57,7 +61,7 @@ def __init__( self, header: str, *, - width: Optional[int] = None, + width: int | None = None, header_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT, header_vert_align: VerticalAlignment = VerticalAlignment.BOTTOM, style_header_text: bool = True, @@ -543,9 +547,9 @@ def __init__( *, column_spacing: int = 2, tab_width: int = 4, - divider_char: Optional[str] = '-', - header_bg: Optional[ansi.BgColor] = None, - data_bg: Optional[ansi.BgColor] = None, + divider_char: str | None = '-', + header_bg: ansi.BgColor | None = None, + data_bg: ansi.BgColor | None = None, ) -> None: """SimpleTable initializer. @@ -737,10 +741,10 @@ def __init__( tab_width: int = 4, column_borders: bool = True, padding: int = 1, - border_fg: Optional[ansi.FgColor] = None, - border_bg: Optional[ansi.BgColor] = None, - header_bg: Optional[ansi.BgColor] = None, - data_bg: Optional[ansi.BgColor] = None, + border_fg: ansi.FgColor | None = None, + border_bg: ansi.BgColor | None = None, + header_bg: ansi.BgColor | None = None, + data_bg: ansi.BgColor | None = None, ) -> None: """BorderedTable initializer. @@ -1035,11 +1039,11 @@ def __init__( tab_width: int = 4, column_borders: bool = True, padding: int = 1, - border_fg: Optional[ansi.FgColor] = None, - border_bg: Optional[ansi.BgColor] = None, - header_bg: Optional[ansi.BgColor] = None, - odd_bg: Optional[ansi.BgColor] = None, - even_bg: Optional[ansi.BgColor] = ansi.Bg.DARK_GRAY, + border_fg: ansi.FgColor | None = None, + border_bg: ansi.BgColor | None = None, + header_bg: ansi.BgColor | None = None, + odd_bg: ansi.BgColor | None = None, + even_bg: ansi.BgColor | None = ansi.Bg.DARK_GRAY, ) -> None: """AlternatingTable initializer. diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 05c5db6c..27c192f3 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -8,12 +8,12 @@ class is used in cmd2.py::run_transcript_tests() """ +from __future__ import annotations + import re import unittest -from collections.abc import Iterator from typing import ( TYPE_CHECKING, - Optional, TextIO, cast, ) @@ -24,6 +24,8 @@ class is used in cmd2.py::run_transcript_tests() ) if TYPE_CHECKING: # pragma: no cover + from collections.abc import Iterator + from cmd2 import ( Cmd, ) @@ -39,7 +41,7 @@ class Cmd2TestCase(unittest.TestCase): See example.py """ - cmdapp: Optional['Cmd'] = None + cmdapp: Cmd | None = None def setUp(self) -> None: """Instructions that will be executed before each test method.""" diff --git a/cmd2/utils.py b/cmd2/utils.py index 1c3506e6..4be3b918 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1,6 +1,7 @@ """Shared utility functions.""" -import argparse +from __future__ import annotations + import collections import contextlib import functools @@ -16,12 +17,14 @@ from collections.abc import Callable, Iterable from difflib import SequenceMatcher from enum import Enum -from typing import TYPE_CHECKING, Any, Optional, TextIO, TypeVar, Union, cast, get_type_hints +from typing import TYPE_CHECKING, Any, TextIO, TypeVar, cast, get_type_hints from . import constants from .argparse_custom import ChoicesProviderFunc, CompleterFunc if TYPE_CHECKING: # pragma: no cover + import argparse + import cmd2 # noqa: F401 PopenTextIO = subprocess.Popen[str] @@ -95,15 +98,15 @@ class Settable: def __init__( self, name: str, - val_type: Union[type[Any], Callable[[Any], Any]], + val_type: type[Any] | Callable[[Any], Any], description: str, settable_object: object, *, - settable_attrib_name: Optional[str] = None, - onchange_cb: Optional[Callable[[str, _T, _T], Any]] = None, - choices: Optional[Iterable[Any]] = None, - choices_provider: Optional[ChoicesProviderFunc] = None, - completer: Optional[CompleterFunc] = None, + settable_attrib_name: str | None = None, + onchange_cb: Callable[[str, _T, _T], Any] | None = None, + choices: Iterable[Any] | None = None, + choices_provider: ChoicesProviderFunc | None = None, + completer: CompleterFunc | None = None, ) -> None: """Settable Initializer. @@ -240,7 +243,7 @@ def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]: return sorted(list_to_sort, key=norm_fold) -def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]: +def try_int_or_force_to_lower_case(input_str: str) -> int | str: """Try 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 @@ -252,7 +255,7 @@ 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]]: +def natural_keys(input_str: str) -> list[int | str]: """Convert a string into a list of integers and strings to support natural sorting (see natural_sort). For example: natural_keys('abc123def') -> ['abc', '123', 'def'] @@ -330,7 +333,7 @@ def expand_user_in_tokens(tokens: list[str]) -> None: tokens[index] = expand_user(tokens[index]) -def find_editor() -> Optional[str]: +def find_editor() -> str | None: """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. @@ -428,7 +431,7 @@ class StdSim: def __init__( self, - inner_stream: Union[TextIO, 'StdSim'], + inner_stream: TextIO | StdSim, *, echo: bool = False, encoding: str = 'utf-8', @@ -469,7 +472,7 @@ def getbytes(self) -> bytes: """Get the internal contents as bytes.""" return bytes(self.buffer.byte_buf) - def read(self, size: Optional[int] = -1) -> str: + def read(self, size: int | None = -1) -> str: """Read from the internal contents as a str and then clear them out. :param size: Number of bytes to read from the stream @@ -551,7 +554,7 @@ class ProcReader: 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: + def __init__(self, proc: PopenTextIO, stdout: StdSim | TextIO, stderr: StdSim | TextIO) -> None: """ProcReader initializer. :param proc: the Popen process being read from @@ -633,7 +636,7 @@ def _reader_thread_func(self, read_stdout: bool) -> None: self._write_bytes(write_stream, available) @staticmethod - def _write_bytes(stream: Union[StdSim, TextIO], to_write: Union[bytes, str]) -> None: + def _write_bytes(stream: StdSim | TextIO, to_write: bytes | str) -> None: """Write bytes to a stream. :param stream: the stream being written to @@ -682,9 +685,9 @@ class RedirectionSavedState: def __init__( self, - self_stdout: Union[StdSim, TextIO], - sys_stdout: Union[StdSim, TextIO], - pipe_proc_reader: Optional[ProcReader], + self_stdout: StdSim | TextIO, + sys_stdout: StdSim | TextIO, + pipe_proc_reader: ProcReader | None, saved_redirecting: bool, ) -> None: """RedirectionSavedState initializer. @@ -730,14 +733,14 @@ def __init__(self) -> None: self.style_dict: dict[int, str] = {} # Indexes into style_dict - self.reset_all: Optional[int] = None - self.fg: Optional[int] = None - self.bg: Optional[int] = None - self.intensity: Optional[int] = None - self.italic: Optional[int] = None - self.overline: Optional[int] = None - self.strikethrough: Optional[int] = None - self.underline: Optional[int] = None + self.reset_all: int | None = None + self.fg: int | None = None + self.bg: int | None = None + self.intensity: int | None = None + self.italic: int | None = None + self.overline: int | None = None + self.strikethrough: int | None = None + self.underline: int | None = None # Read the previous styles in order and keep track of their states style_state = StyleState() @@ -800,7 +803,7 @@ def align_text( alignment: TextAlignment, *, fill_char: str = ' ', - width: Optional[int] = None, + width: int | None = None, tab_width: int = 4, truncate: bool = False, ) -> str: @@ -922,7 +925,7 @@ def align_text( def align_left( - text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False + text: str, *, fill_char: str = ' ', width: int | None = 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. @@ -945,7 +948,7 @@ def align_left( def align_center( - text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False + text: str, *, fill_char: str = ' ', width: int | None = 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. @@ -968,7 +971,7 @@ def align_center( def align_right( - text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False + text: str, *, fill_char: str = ' ', width: int | None = 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. @@ -1097,7 +1100,7 @@ def get_styles_dict(text: str) -> dict[int, str]: return styles -def categorize(func: Union[Callable[..., Any], Iterable[Callable[..., Any]]], category: str) -> None: +def categorize(func: Callable[..., Any] | Iterable[Callable[..., Any]], category: str) -> None: """Categorize a function. The help command output will group the passed function under the @@ -1128,7 +1131,7 @@ def do_echo(self, arglist): setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category) -def get_defining_class(meth: Callable[..., Any]) -> Optional[type[Any]]: +def get_defining_class(meth: Callable[..., Any]) -> type[Any] | None: """Attempt to resolve the class that defined a method. Inspired by implementation published here: @@ -1225,8 +1228,8 @@ 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]: + requested_command: str, options: Iterable[str], similarity_function_to_use: Callable[[str, str], float] | None = None +) -> str | None: """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 diff --git a/examples/alias_startup.py b/examples/alias_startup.py index f6e401a0..e262d535 100755 --- a/examples/alias_startup.py +++ b/examples/alias_startup.py @@ -4,6 +4,8 @@ 2) How to run an initialization script at startup. """ +from __future__ import annotations + import os import cmd2 diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py index 5fe262d4..fc355fd5 100755 --- a/examples/arg_decorators.py +++ b/examples/arg_decorators.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 """An example demonstrating how use one of cmd2's argument parsing decorators.""" -import argparse +from __future__ import annotations + import os +from typing import TYPE_CHECKING import cmd2 +if TYPE_CHECKING: # pragma: no cover + import argparse + class ArgparsingApp(cmd2.Cmd): def __init__(self) -> None: diff --git a/examples/arg_print.py b/examples/arg_print.py index 506e9225..ea3c8dd6 100755 --- a/examples/arg_print.py +++ b/examples/arg_print.py @@ -9,6 +9,8 @@ It also serves as an example of how to create shortcuts. """ +from __future__ import annotations + import cmd2 diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index 43cad367..5d838e8f 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -1,7 +1,9 @@ #!/usr/bin/env python """A simple example demonstrating how to integrate tab completion with argparse-based commands.""" -import argparse +from __future__ import annotations + +from typing import TYPE_CHECKING from cmd2 import ( Cmd, @@ -12,6 +14,9 @@ with_argparser, ) +if TYPE_CHECKING: # pragma: no cover + import argparse + # Data source for argparse.choices food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] diff --git a/examples/async_printing.py b/examples/async_printing.py index 5655a62f..a8e9b773 100755 --- a/examples/async_printing.py +++ b/examples/async_printing.py @@ -3,6 +3,8 @@ and changes the window title. """ +from __future__ import annotations + import random import threading import time diff --git a/examples/basic.py b/examples/basic.py index 20ebe20a..eae48cae 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -8,6 +8,8 @@ 6) Shell-like capabilities. """ +from __future__ import annotations + import cmd2 from cmd2 import ( Bg, diff --git a/examples/basic_completion.py b/examples/basic_completion.py index fd3a5c63..4e579f85 100755 --- a/examples/basic_completion.py +++ b/examples/basic_completion.py @@ -11,6 +11,8 @@ argparse-based completion. For an example integrating tab completion with argparse, see argparse_completion.py """ +from __future__ import annotations + import functools import cmd2 diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index dd265074..ce2f93c9 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -10,6 +10,8 @@ """ +from __future__ import annotations + import argparse import random diff --git a/examples/colors.py b/examples/colors.py index fad3c958..d805f6d9 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -21,6 +21,8 @@ regardless of the output destination """ +from __future__ import annotations + import cmd2 from cmd2 import ( Bg, diff --git a/examples/custom_parser.py b/examples/custom_parser.py index a79a65b8..17ad8d15 100644 --- a/examples/custom_parser.py +++ b/examples/custom_parser.py @@ -1,5 +1,7 @@ """Defines the CustomParser used with override_parser.py example.""" +from __future__ import annotations + import sys from cmd2 import ( diff --git a/examples/decorator_example.py b/examples/decorator_example.py index 736c729e..bd935dcd 100755 --- a/examples/decorator_example.py +++ b/examples/decorator_example.py @@ -10,10 +10,15 @@ verifying that the output produced matches the transcript. """ -import argparse +from __future__ import annotations + +from typing import TYPE_CHECKING import cmd2 +if TYPE_CHECKING: # pragma: no cover + import argparse + class CmdLineApp(cmd2.Cmd): """Example cmd2 application.""" diff --git a/examples/default_categories.py b/examples/default_categories.py index e0f26b99..54a3e4a6 100755 --- a/examples/default_categories.py +++ b/examples/default_categories.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Simple example demonstrating basic CommandSet usage.""" +from __future__ import annotations + import cmd2 from cmd2 import ( CommandSet, diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py index 137f30c7..e00bf8f9 100755 --- a/examples/dynamic_commands.py +++ b/examples/dynamic_commands.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """A simple example demonstrating how do_* commands can be created in a loop.""" +from __future__ import annotations + import functools import cmd2 diff --git a/examples/environment.py b/examples/environment.py index 706f150f..3ed685a8 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A sample application for cmd2 demonstrating customized environment parameters.""" +from __future__ import annotations + import cmd2 diff --git a/examples/event_loops.py b/examples/event_loops.py index aca43420..8e0d8c09 100755 --- a/examples/event_loops.py +++ b/examples/event_loops.py @@ -6,6 +6,8 @@ This opens up the possibility of registering cmd2 input with event loops, like asyncio, without occupying the main loop. """ +from __future__ import annotations + import cmd2 diff --git a/examples/example.py b/examples/example.py index 20918152..547cc07b 100755 --- a/examples/example.py +++ b/examples/example.py @@ -9,6 +9,8 @@ the transcript. """ +from __future__ import annotations + import random import cmd2 diff --git a/examples/exit_code.py b/examples/exit_code.py index bfce8c90..0cd3bfdd 100755 --- a/examples/exit_code.py +++ b/examples/exit_code.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application.""" +from __future__ import annotations + import cmd2 diff --git a/examples/first_app.py b/examples/first_app.py index c82768a3..e42a0d35 100755 --- a/examples/first_app.py +++ b/examples/first_app.py @@ -11,6 +11,8 @@ * History """ +from __future__ import annotations + import cmd2 diff --git a/examples/hello_cmd2.py b/examples/hello_cmd2.py index a480aa5e..bcbb2cf6 100755 --- a/examples/hello_cmd2.py +++ b/examples/hello_cmd2.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """This is intended to be a completely bare-bones cmd2 application suitable for rapid testing and debugging.""" +from __future__ import annotations + from cmd2 import ( cmd2, ) diff --git a/examples/help_categories.py b/examples/help_categories.py index 7a9b4aca..493f2154 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -4,6 +4,8 @@ It also demonstrates the effects of decorator order when it comes to argparse errors occurring. """ +from __future__ import annotations + import functools import cmd2 diff --git a/examples/hooks.py b/examples/hooks.py index ccb9a838..b7768859 100755 --- a/examples/hooks.py +++ b/examples/hooks.py @@ -7,6 +7,8 @@ """ +from __future__ import annotations + import re import cmd2 diff --git a/examples/initialization.py b/examples/initialization.py index 22de3ff2..e968cd2a 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -12,6 +12,8 @@ 10) How to make custom attributes settable at runtime. """ +from __future__ import annotations + import cmd2 from cmd2 import ( Bg, diff --git a/examples/migrating.py b/examples/migrating.py index 9c79d488..9bf9084e 100755 --- a/examples/migrating.py +++ b/examples/migrating.py @@ -2,6 +2,8 @@ """A sample cmd application that shows how to trivially migrate a cmd application to use cmd2.""" # import cmd2 as cmd # noqa: ERA001 +from __future__ import annotations + import cmd # Comment this line and uncomment the one above to migrate to cmd2 import random diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py index 8ef0a9d0..78ccfadd 100644 --- a/examples/modular_commands/commandset_basic.py +++ b/examples/modular_commands/commandset_basic.py @@ -1,5 +1,7 @@ """A simple example demonstrating a loadable command set.""" +from __future__ import annotations + from cmd2 import ( CommandSet, CompletionError, diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py index d1e157b9..fdeac5fb 100644 --- a/examples/modular_commands/commandset_complex.py +++ b/examples/modular_commands/commandset_complex.py @@ -1,9 +1,14 @@ """Test CommandSet.""" -import argparse +from __future__ import annotations + +from typing import TYPE_CHECKING import cmd2 +if TYPE_CHECKING: # pragma: no cover + import argparse + @cmd2.with_default_category('Fruits') class CommandSetA(cmd2.CommandSet): diff --git a/examples/modular_commands/commandset_custominit.py b/examples/modular_commands/commandset_custominit.py index 8bc0474d..43d6dbbf 100644 --- a/examples/modular_commands/commandset_custominit.py +++ b/examples/modular_commands/commandset_custominit.py @@ -1,5 +1,7 @@ """A simple example demonstrating a loadable command set.""" +from __future__ import annotations + from cmd2 import ( Cmd, CommandSet, diff --git a/examples/modular_commands_basic.py b/examples/modular_commands_basic.py index c681a389..cdfb46e2 100755 --- a/examples/modular_commands_basic.py +++ b/examples/modular_commands_basic.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Simple example demonstrating basic CommandSet usage.""" +from __future__ import annotations + import cmd2 from cmd2 import ( CommandSet, diff --git a/examples/modular_commands_dynamic.py b/examples/modular_commands_dynamic.py index 163c9dc8..ce7178c5 100755 --- a/examples/modular_commands_dynamic.py +++ b/examples/modular_commands_dynamic.py @@ -7,7 +7,9 @@ on which CommandSets are loaded """ -import argparse +from __future__ import annotations + +from typing import TYPE_CHECKING import cmd2 from cmd2 import ( @@ -17,6 +19,9 @@ with_default_category, ) +if TYPE_CHECKING: # pragma: no cover + import argparse + @with_default_category('Fruits') class LoadableFruits(CommandSet): diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index f03ea38d..a7dba0e4 100755 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -3,9 +3,9 @@ with examples of how to integrate tab completion with argparse-based commands. """ -import argparse -from collections.abc import Iterable -from typing import Optional +from __future__ import annotations + +from typing import TYPE_CHECKING from modular_commands.commandset_basic import ( # noqa: F401 BasicCompletionCommandSet, @@ -24,9 +24,13 @@ with_argparser, ) +if TYPE_CHECKING: # pragma: no cover + import argparse + from collections.abc import Iterable + class WithCommandSets(Cmd): - def __init__(self, command_sets: Optional[Iterable[CommandSet]] = None) -> None: + def __init__(self, command_sets: Iterable[CommandSet] | None = None) -> None: super().__init__(command_sets=command_sets) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] diff --git a/examples/modular_subcommands.py b/examples/modular_subcommands.py index f1dbd024..6297d151 100755 --- a/examples/modular_subcommands.py +++ b/examples/modular_subcommands.py @@ -10,7 +10,9 @@ subcommands to the `cut` command will change depending on which CommandSets are loaded. """ -import argparse +from __future__ import annotations + +from typing import TYPE_CHECKING import cmd2 from cmd2 import ( @@ -20,6 +22,9 @@ with_default_category, ) +if TYPE_CHECKING: # pragma: no cover + import argparse + @with_default_category('Fruits') class LoadableFruits(CommandSet): diff --git a/examples/override_parser.py b/examples/override_parser.py index 2d4a0f9c..1bd6410e 100755 --- a/examples/override_parser.py +++ b/examples/override_parser.py @@ -6,6 +6,8 @@ # First set a value called argparse.cmd2_parser_module with the module that defines the custom parser. # See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser_type() # with the custom parser's type. +from __future__ import annotations + import argparse argparse.cmd2_parser_module = 'custom_parser' diff --git a/examples/paged_output.py b/examples/paged_output.py index 935bdd2e..2e851a69 100755 --- a/examples/paged_output.py +++ b/examples/paged_output.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A simple example demonstrating the using paged output via the ppaged() method.""" +from __future__ import annotations + import os import cmd2 diff --git a/examples/persistent_history.py b/examples/persistent_history.py index d2ae8cef..84b321b7 100755 --- a/examples/persistent_history.py +++ b/examples/persistent_history.py @@ -5,6 +5,8 @@ across invocations of your cmd2 application. This can make it much easier for them to use your application. """ +from __future__ import annotations + import cmd2 diff --git a/examples/pirate.py b/examples/pirate.py index b15dae4f..f3ecb9d8 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -5,6 +5,8 @@ It demonstrates many features of cmd2. """ +from __future__ import annotations + import cmd2 from cmd2 import ( Fg, diff --git a/examples/pretty_print.py b/examples/pretty_print.py index 9cdc5715..9a0f396b 100755 --- a/examples/pretty_print.py +++ b/examples/pretty_print.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """A simple example demonstrating use of cmd2.Cmd.ppretty().""" +from __future__ import annotations + import cmd2 data = { diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 393e31fd..50322341 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -20,6 +20,8 @@ example for one way in which this can be done. """ +from __future__ import annotations + import os import cmd2 diff --git a/examples/read_input.py b/examples/read_input.py index 40861770..977d738d 100755 --- a/examples/read_input.py +++ b/examples/read_input.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion.""" +from __future__ import annotations + import contextlib import cmd2 diff --git a/examples/remove_builtin_commands.py b/examples/remove_builtin_commands.py index 64acd17d..d1801d31 100755 --- a/examples/remove_builtin_commands.py +++ b/examples/remove_builtin_commands.py @@ -8,6 +8,8 @@ Commands can also be removed entirely by using Python's "del". """ +from __future__ import annotations + import cmd2 diff --git a/examples/remove_settable.py b/examples/remove_settable.py index c2c33889..99a7d23e 100755 --- a/examples/remove_settable.py +++ b/examples/remove_settable.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters.""" +from __future__ import annotations + import cmd2 diff --git a/examples/scripts/arg_printer.py b/examples/scripts/arg_printer.py index aca0f003..bc9748e8 100755 --- a/examples/scripts/arg_printer.py +++ b/examples/scripts/arg_printer.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +from __future__ import annotations + import os import sys diff --git a/examples/scripts/conditional.py b/examples/scripts/conditional.py index 99c442de..5d5c2515 100644 --- a/examples/scripts/conditional.py +++ b/examples/scripts/conditional.py @@ -8,6 +8,8 @@ application instance. Note: self only exists in this environment if self_in_py is True. """ +from __future__ import annotations + import os import sys diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py index 41636b08..c7b36202 100644 --- a/examples/scripts/save_help_text.py +++ b/examples/scripts/save_help_text.py @@ -2,6 +2,8 @@ This is meant to be run within a cmd2 session using run_pyscript. """ +from __future__ import annotations + import argparse import os import sys diff --git a/examples/scripts/script.py b/examples/scripts/script.py index cca0130c..987a80e8 100644 --- a/examples/scripts/script.py +++ b/examples/scripts/script.py @@ -1,3 +1,5 @@ """Trivial example of a Python script which can be run inside a cmd2 application.""" +from __future__ import annotations + print("This is a python script running ...") diff --git a/examples/subcommands.py b/examples/subcommands.py index b2768cff..c8335c2b 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -5,6 +5,8 @@ and provides separate contextual help. """ +from __future__ import annotations + import cmd2 sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] diff --git a/examples/table_creation.py b/examples/table_creation.py index 00a45d29..eecc41d1 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """Examples of using the cmd2 table creation API.""" +from __future__ import annotations + import functools import sys from typing import Any diff --git a/examples/unicode_commands.py b/examples/unicode_commands.py index 3321e636..be177cea 100755 --- a/examples/unicode_commands.py +++ b/examples/unicode_commands.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """A simple example demonstrating support for unicode command names.""" +from __future__ import annotations + import math import cmd2 diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py index 94796e7b..3178c28f 100644 --- a/plugins/ext_test/cmd2_ext_test/__init__.py +++ b/plugins/ext_test/cmd2_ext_test/__init__.py @@ -3,6 +3,8 @@ Allows developers to exercise their cmd2 application using the PyScript interface """ +from __future__ import annotations + import importlib.metadata as importlib_metadata try: 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 1cb45f60..76a06af0 100644 --- a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py +++ b/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py @@ -1,8 +1,9 @@ """External test interface plugin""" +from __future__ import annotations + from typing import ( TYPE_CHECKING, - Optional, ) import cmd2 @@ -29,7 +30,7 @@ def __init__(self, *args, **kwargs): # code placed here runs after cmd2 initializes self._pybridge = cmd2.py_bridge.PyBridge(self) - def app_cmd(self, command: str, echo: Optional[bool] = None) -> cmd2.CommandResult: + def app_cmd(self, command: str, echo: bool = False) -> cmd2.CommandResult: """ Run the application command diff --git a/plugins/ext_test/examples/example.py b/plugins/ext_test/examples/example.py index c9c0ee26..3c3c7017 100644 --- a/plugins/ext_test/examples/example.py +++ b/plugins/ext_test/examples/example.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import cmd2_ext_test import cmd2 diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py index d8aa344b..d9c11c5c 100644 --- a/plugins/ext_test/noxfile.py +++ b/plugins/ext_test/noxfile.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import nox diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index b274959c..12835c84 100644 --- a/plugins/ext_test/setup.py +++ b/plugins/ext_test/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import setuptools diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py index b7f36937..8723546f 100644 --- a/plugins/ext_test/tasks.py +++ b/plugins/ext_test/tasks.py @@ -6,6 +6,8 @@ - setuptools >= 39.1.0 """ +from __future__ import annotations + import contextlib import os import pathlib diff --git a/plugins/ext_test/tests/test_ext_test.py b/plugins/ext_test/tests/test_ext_test.py index df9216d8..da485a76 100644 --- a/plugins/ext_test/tests/test_ext_test.py +++ b/plugins/ext_test/tests/test_ext_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import cmd2_ext_test import pytest diff --git a/plugins/tasks.py b/plugins/tasks.py index b2e2024e..cdfce227 100644 --- a/plugins/tasks.py +++ b/plugins/tasks.py @@ -6,6 +6,8 @@ - setuptools >= 39.1.0 """ +from __future__ import annotations + import pathlib import invoke diff --git a/plugins/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py index 3d4703d5..c6842b50 100644 --- a/plugins/template/cmd2_myplugin/__init__.py +++ b/plugins/template/cmd2_myplugin/__init__.py @@ -3,6 +3,8 @@ An overview of what myplugin does. """ +from __future__ import annotations + import importlib.metadata as importlib_metadata from .myplugin import ( # noqa: F401 diff --git a/plugins/template/cmd2_myplugin/myplugin.py b/plugins/template/cmd2_myplugin/myplugin.py index 37639a5c..1588282a 100644 --- a/plugins/template/cmd2_myplugin/myplugin.py +++ b/plugins/template/cmd2_myplugin/myplugin.py @@ -1,12 +1,15 @@ """An example cmd2 plugin.""" +from __future__ import annotations + import functools -from collections.abc import Callable from typing import TYPE_CHECKING -import cmd2 - if TYPE_CHECKING: # pragma: no cover + from collections.abc import Callable + + import cmd2 + _Base = cmd2.Cmd else: _Base = object diff --git a/plugins/template/examples/example.py b/plugins/template/examples/example.py index 055970b1..00823bc6 100644 --- a/plugins/template/examples/example.py +++ b/plugins/template/examples/example.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import cmd2_myplugin import cmd2 diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py index cac9f917..8f7cef7b 100644 --- a/plugins/template/noxfile.py +++ b/plugins/template/noxfile.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import nox diff --git a/plugins/template/setup.py b/plugins/template/setup.py index 3eed7f28..04da635d 100644 --- a/plugins/template/setup.py +++ b/plugins/template/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import setuptools diff --git a/plugins/template/tasks.py b/plugins/template/tasks.py index 93a9c1a1..a435cfe4 100644 --- a/plugins/template/tasks.py +++ b/plugins/template/tasks.py @@ -1,5 +1,7 @@ """Development related tasks to be run with 'invoke'.""" +from __future__ import annotations + import contextlib import os import pathlib diff --git a/plugins/template/tests/test_myplugin.py b/plugins/template/tests/test_myplugin.py index 54e919f5..931b5711 100644 --- a/plugins/template/tests/test_myplugin.py +++ b/plugins/template/tests/test_myplugin.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import cmd2_myplugin from cmd2 import ( diff --git a/pyproject.toml b/pyproject.toml index cb14d536..e240a20d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -233,7 +233,6 @@ ignore = [ "E111", # Conflicts with ruff format "E114", # Conflicts with ruff format "E117", # Conflicts with ruff format - "FA100", # Adding from __future__ import annotations screws up cmd2 because of how use inspect to validate type annotations at runtime "ISC002", # Conflicts with ruff format "Q000", # Conflicts with ruff format "Q001", # Conflicts with ruff format @@ -241,9 +240,7 @@ ignore = [ "Q003", # Conflicts with ruff format "TC006", # Add quotes to type expression in typing.cast() (not needed except for forward references) "TRY003", # Avoid specifying long messages outside the exception class (force custom exceptions for everything) - "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 ] @@ -257,6 +254,9 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" mccabe.max-complexity = 49 +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + [tool.ruff.lint.per-file-ignores] # Module level import not at top of file and unused import "cmd2/__init__.py" = ["E402", "F401"] diff --git a/tasks.py b/tasks.py index f6b9d7ff..76406031 100644 --- a/tasks.py +++ b/tasks.py @@ -6,27 +6,31 @@ - setuptools >= 39.1.0 """ +from __future__ import annotations + import contextlib import os import pathlib import re import shutil import sys -from typing import Union +from typing import TYPE_CHECKING import invoke -from invoke.context import Context from plugins import ( tasks as plugin_tasks, ) +if TYPE_CHECKING: # pragma: no cover + from invoke.context import Context + TASK_ROOT = pathlib.Path(__file__).resolve().parent TASK_ROOT_STR = str(TASK_ROOT) # shared function -def rmrf(items: Union[str, list[str], set[str]], verbose: bool = True) -> None: +def rmrf(items: str | list[str] | set[str], verbose: bool = True) -> None: """Silently remove a list of directories or files.""" if isinstance(items, str): items = [items] diff --git a/tests/conftest.py b/tests/conftest.py index b9c64375..5bd545bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,13 @@ """Cmd2 unit/functional testing""" +from __future__ import annotations + import argparse import sys from contextlib import ( redirect_stderr, redirect_stdout, ) -from typing import ( - Optional, - Union, -) from unittest import ( mock, ) @@ -25,9 +23,7 @@ ) -def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None -) -> None: +def verify_help_text(cmd2_app: cmd2.Cmd, help_output: str | list[str], verbose_strings: list[str] | None = None) -> None: """This function verifies that all expected commands are present in the help text. :param cmd2_app: instance of cmd2.Cmd @@ -152,7 +148,7 @@ def base_app(): odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] -def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: +def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> str | None: """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 diff --git a/tests/pyscript/echo.py b/tests/pyscript/echo.py index c5999355..57d31a3d 100644 --- a/tests/pyscript/echo.py +++ b/tests/pyscript/echo.py @@ -1,4 +1,6 @@ # Tests echo argument to app() +from __future__ import annotations + app.cmd_echo = False # echo defaults to current setting which is False, so this help text should not be echoed to pytest's stdout diff --git a/tests/pyscript/environment.py b/tests/pyscript/environment.py index 758c8500..c51587e3 100644 --- a/tests/pyscript/environment.py +++ b/tests/pyscript/environment.py @@ -1,4 +1,6 @@ # Tests that cmd2 populates __name__, __file__, and sets sys.path[0] to our directory +from __future__ import annotations + import os import sys diff --git a/tests/pyscript/help.py b/tests/pyscript/help.py index 480c6cd7..235c2e65 100644 --- a/tests/pyscript/help.py +++ b/tests/pyscript/help.py @@ -1,3 +1,5 @@ +from __future__ import annotations + app.cmd_echo = True app('help') diff --git a/tests/pyscript/py_locals.py b/tests/pyscript/py_locals.py index aa1e0893..29a553e7 100644 --- a/tests/pyscript/py_locals.py +++ b/tests/pyscript/py_locals.py @@ -1,4 +1,5 @@ # Tests how much a pyscript can affect cmd2.Cmd.py_locals +from __future__ import annotations del [locals()["test_var"]] my_list.append(2) diff --git a/tests/pyscript/pyscript_dir.py b/tests/pyscript/pyscript_dir.py index 14a70a31..d84845dc 100644 --- a/tests/pyscript/pyscript_dir.py +++ b/tests/pyscript/pyscript_dir.py @@ -1,3 +1,5 @@ +from __future__ import annotations + out = dir(app) out.sort() print(out) diff --git a/tests/pyscript/raises_exception.py b/tests/pyscript/raises_exception.py index 9883a2b8..8860346b 100644 --- a/tests/pyscript/raises_exception.py +++ b/tests/pyscript/raises_exception.py @@ -1,3 +1,5 @@ """Example demonstrating what happens when a Python script raises an exception""" +from __future__ import annotations + x = 1 + 'blue' diff --git a/tests/pyscript/recursive.py b/tests/pyscript/recursive.py index f71234b8..b3dc6147 100644 --- a/tests/pyscript/recursive.py +++ b/tests/pyscript/recursive.py @@ -1,5 +1,7 @@ """Example demonstrating that calling run_pyscript recursively inside another Python script isn't allowed""" +from __future__ import annotations + import os import sys diff --git a/tests/pyscript/self_in_py.py b/tests/pyscript/self_in_py.py index ee26293f..051dc194 100644 --- a/tests/pyscript/self_in_py.py +++ b/tests/pyscript/self_in_py.py @@ -1,4 +1,6 @@ # Tests self_in_py in pyscripts +from __future__ import annotations + if 'self' in globals(): print("I see self") else: diff --git a/tests/pyscript/stdout_capture.py b/tests/pyscript/stdout_capture.py index 5cc0cf3a..3bd0cddf 100644 --- a/tests/pyscript/stdout_capture.py +++ b/tests/pyscript/stdout_capture.py @@ -1,4 +1,6 @@ # This script demonstrates when output of a command finalization hook is captured by a pyscript app() call +from __future__ import annotations + import sys # The unit test framework passes in the string being printed by the command finalization hook diff --git a/tests/pyscript/stop.py b/tests/pyscript/stop.py index 31b587bd..2a5a4a6f 100644 --- a/tests/pyscript/stop.py +++ b/tests/pyscript/stop.py @@ -1,3 +1,5 @@ +from __future__ import annotations + app.cmd_echo = True app('help') diff --git a/tests/script.py b/tests/script.py index cca0130c..987a80e8 100644 --- a/tests/script.py +++ b/tests/script.py @@ -1,3 +1,5 @@ """Trivial example of a Python script which can be run inside a cmd2 application.""" +from __future__ import annotations + print("This is a python script running ...") diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 84119072..6660b93c 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -1,5 +1,7 @@ """Unit testing for cmd2/ansi.py module""" +from __future__ import annotations + import pytest from cmd2 import ( diff --git a/tests/test_argparse.py b/tests/test_argparse.py index e03edb37..d15071f1 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -1,7 +1,8 @@ """Cmd2 testing for argument parsing""" +from __future__ import annotations + import argparse -from typing import Optional import pytest @@ -32,7 +33,7 @@ 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) -> None: + def do_say(self, args, *, keyword_arg: str | None = None) -> None: """Repeat what you tell me to. @@ -71,7 +72,7 @@ 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) -> None: + def do_arglist(self, arglist, *, keyword_arg: str | None = None) -> None: if isinstance(arglist, list): self.stdout.write('True') else: @@ -93,7 +94,7 @@ 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) -> None: + def do_speak(self, args, extra, *, keyword_arg: str | None = None) -> None: """Repeat what you tell me to.""" words = [] for word in extra: diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index f6561321..51a6e941 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -1,5 +1,7 @@ """Unit/functional testing for argparse completer in cmd2""" +from __future__ import annotations + import argparse import numbers from typing import cast diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py index bd79910e..41922a5f 100644 --- a/tests/test_argparse_custom.py +++ b/tests/test_argparse_custom.py @@ -1,5 +1,7 @@ """Unit/functional testing for argparse customizations in cmd2""" +from __future__ import annotations + import argparse import pytest diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 8e23b7ab..7f852b78 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1,5 +1,7 @@ """Cmd2 unit/functional testing""" +from __future__ import annotations + import builtins import io import os diff --git a/tests/test_completion.py b/tests/test_completion.py index 1d9e9256..e391cd93 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -4,6 +4,8 @@ file system paths, and shell commands. """ +from __future__ import annotations + import enum import os import sys diff --git a/tests/test_history.py b/tests/test_history.py index 7b2a3a7c..a7b31586 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -1,5 +1,7 @@ """Test history functions of cmd2""" +from __future__ import annotations + import contextlib import os import tempfile diff --git a/tests/test_future_annotations.py b/tests/test_no_future_annotations.py similarity index 92% rename from tests/test_future_annotations.py rename to tests/test_no_future_annotations.py index 81e5953a..baacaef5 100644 --- a/tests/test_future_annotations.py +++ b/tests/test_no_future_annotations.py @@ -1,6 +1,4 @@ -from __future__ import annotations - -import cmd2 +import cmd2 # noqa: I002 from .conftest import normalize, run_cmd diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 711868ca..f3fada29 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -1,5 +1,7 @@ """Test the parsing logic in parsing.py""" +from __future__ import annotations + import dataclasses import pytest diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 56c2f2d5..0a311649 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,8 +1,9 @@ """Test plugin infrastructure and hooks.""" -import argparse +from __future__ import annotations + import sys -from typing import NoReturn +from typing import TYPE_CHECKING, NoReturn from unittest import ( mock, ) @@ -17,6 +18,9 @@ with_argparser, ) +if TYPE_CHECKING: # pragma: no cover + import argparse + class Plugin: """A mixin class for testing hook registration and calling""" diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index a64f77ba..a8fd338e 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -1,5 +1,7 @@ """Unit/functional testing for run_pytest in cmd2""" +from __future__ import annotations + import builtins import os from unittest import ( diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index caf19b7e..fa304f0b 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -1,5 +1,7 @@ """Unit testing for cmd2/table_creator.py module""" +from __future__ import annotations + import pytest from cmd2 import ( diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 0739c0c7..ebc81eae 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -1,5 +1,7 @@ """Cmd2 functional testing based on transcript""" +from __future__ import annotations + import os import random import re diff --git a/tests/test_utils.py b/tests/test_utils.py index 334b1300..ff749c81 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,7 @@ """Unit testing for cmd2/utils.py module.""" +from __future__ import annotations + import os import signal import sys diff --git a/tests/test_utils_defining_class.py b/tests/test_utils_defining_class.py index f0c27895..a53f2ed0 100644 --- a/tests/test_utils_defining_class.py +++ b/tests/test_utils_defining_class.py @@ -1,5 +1,7 @@ """Unit testing for get_defining_class in cmd2/utils.py module.""" +from __future__ import annotations + import functools import cmd2.utils as cu diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index 171f4a29..63436e01 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -1,14 +1,12 @@ """Cmd2 unit/functional testing""" +from __future__ import annotations + import sys from contextlib import ( redirect_stderr, redirect_stdout, ) -from typing import ( - Optional, - Union, -) from unittest import ( mock, ) @@ -27,9 +25,7 @@ ) -def verify_help_text( - cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None -) -> None: +def verify_help_text(cmd2_app: cmd2.Cmd, help_output: str | list[str], verbose_strings: list[str] | None = None) -> None: """This function verifies that all expected commands are present in the help text. :param cmd2_app: instance of cmd2.Cmd @@ -135,7 +131,7 @@ def base_app(): odd_file_names = ['nothingweird', 'has spaces', '"is_double_quoted"', "'is_single_quoted'"] -def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: +def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> str | None: """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 diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py index 5f4645d5..5407d7d0 100644 --- a/tests_isolated/test_commandset/test_argparse_subcommands.py +++ b/tests_isolated/test_commandset/test_argparse_subcommands.py @@ -1,5 +1,7 @@ """reproduces test_argparse.py except with SubCommands""" +from __future__ import annotations + import pytest import cmd2 diff --git a/tests_isolated/test_commandset/test_categories.py b/tests_isolated/test_commandset/test_categories.py index 8150c5e7..67f8efa8 100644 --- a/tests_isolated/test_commandset/test_categories.py +++ b/tests_isolated/test_commandset/test_categories.py @@ -1,5 +1,7 @@ """Simple example demonstrating basic CommandSet usage.""" +from __future__ import annotations + from typing import Any import cmd2 diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 7498e145..0dbefa9a 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -1,5 +1,7 @@ """Test CommandSet""" +from __future__ import annotations + import argparse import signal