diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 48a4a2ed..57049b58 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -62,7 +62,7 @@ See the `dependencies` list under the `[project]` heading in [pyproject.toml](.. | Prerequisite | Minimum Version | Purpose | | --------------------------------------------------- | --------------- | -------------------------------------- | -| [python](https://www.python.org/downloads/) | `3.9` | Python programming language | +| [python](https://www.python.org/downloads/) | `3.10` | Python programming language | | [pyperclip](https://github.com/asweigart/pyperclip) | `1.8` | Cross-platform clipboard functions | | [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.10` | Measure the displayed width of unicode | @@ -520,13 +520,11 @@ on how to do it. 4. The title (also called the subject) of your PR should be descriptive of your changes and succinctly indicate what is being fixed - - **Do not add the issue number in the PR title or commit message** - Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation` 5. In the body of your PR include a more detailed summary of the changes you made and why - - If the PR is meant to fix an existing bug/issue, then, at the end of your PR's description, append the keyword `closes` and #xxxx (where xxxx is the issue number). Example: `closes #1337`. This tells GitHub to close the existing issue if the PR is merged. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 51d75f65..e57d883d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] fail-fast: false runs-on: ${{ matrix.os }} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 45e65d84..89d5b538 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] fail-fast: false defaults: run: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88e67c64..8891368f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.12.4" + rev: "v0.12.7" hooks: - id: ruff-format args: [--config=pyproject.toml] @@ -21,5 +21,5 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.5.3 - - prettier-plugin-toml@2.0.5 + - prettier@3.6.2 + - prettier-plugin-toml@2.0.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e97ffaa..f4fd32f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,12 @@ ## 3.0.0 (TBD) - Breaking Changes - + - `cmd2` 3.0 supports Python 3.10+ (removed support for Python 3.9) - No longer setting parser's `prog` value in `with_argparser()` since it gets set in `Cmd._build_parser()`. This code had previously been restored to support backward compatibility in `cmd2` 2.0 family. - Enhancements - - Simplified the process to set a custom parser for `cmd2's` built-in commands. See [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py) example for more details. @@ -30,7 +29,6 @@ ## 2.6.2 (June 26, 2025) - Enhancements - - Added explicit support for free-threaded versions of Python, starting with version 3.14 - Bug Fixes @@ -1316,12 +1314,10 @@ ## 0.8.5 (April 15, 2018) - Bug Fixes - - Fixed a bug with all argument decorators where the wrapped function wasn't returning a value and thus couldn't cause the cmd2 app to quit - Enhancements - - Added support for verbose help with -v where it lists a brief summary of what each command does - Added support for categorizing commands into groups within the help menu @@ -1353,12 +1349,10 @@ ## 0.8.3 (April 09, 2018) - Bug Fixes - - Fixed `help` command not calling functions for help topics - Fixed not being able to use quoted paths when redirecting with `<` and `>` - Enhancements - - Tab completion has been overhauled and now supports completion of strings with quotes and spaces. - Tab completion will automatically add an opening quote if a string with a space is completed. diff --git a/README.md b/README.md index bb412042..688ed57a 100755 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u pip install -U cmd2 ``` -cmd2 works with Python 3.9+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party +cmd2 works with Python 3.10+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies. It works with both conventional CPython and free-threaded variants. For information on other installation options, see diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 929d77bd..76c540c8 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -11,7 +11,6 @@ from typing import ( IO, Any, - Optional, cast, ) @@ -924,14 +923,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 1b1efce7..5aeef609 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -12,8 +12,6 @@ from typing import ( IO, TYPE_CHECKING, - Optional, - Union, cast, ) @@ -105,8 +103,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 @@ -141,7 +139,7 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None: :param flag_arg_state: information about the unfinished flag action. """ arg = f'{argparse._get_action_name(flag_arg_state.action)}' - err = f'{generate_range_error(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max))}' + err = f'{generate_range_error(cast(int, flag_arg_state.min), cast(int | float, flag_arg_state.max))}' error = f"Error: argument {arg}: {err} ({flag_arg_state.count} entered)" super().__init__(error) @@ -163,7 +161,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. @@ -203,7 +201,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. @@ -228,10 +226,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] = [] @@ -523,7 +521,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): @@ -625,7 +623,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in break return [] - def print_help(self, tokens: list[str], file: Optional[IO[str]] = None) -> None: + def print_help(self, tokens: list[str], file: IO[str] | None = None) -> None: """Supports cmd2's help command in the printing of help text. :param tokens: arguments passed to help command @@ -636,7 +634,7 @@ def print_help(self, tokens: list[str], file: Optional[IO[str]] = None) -> None: # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter. if tokens and self._subcommand_action is not None: parser = cast( - Optional[argparse.ArgumentParser], + argparse.ArgumentParser | None, self._subcommand_action.choices.get(tokens[0]), ) @@ -657,7 +655,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. @@ -665,7 +663,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 95a3644b..df20ceb6 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -240,9 +240,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) Any, ClassVar, NoReturn, - Optional, Protocol, - Union, cast, runtime_checkable, ) @@ -392,7 +390,7 @@ def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: # pr """Enable instances to be called like functions.""" -ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens] +ChoicesProviderFunc = ChoicesProviderFuncBase | ChoicesProviderFuncWithTokens @runtime_checkable @@ -425,7 +423,7 @@ def __call__( """Enable instances to be called like functions.""" -CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] +CompleterFunc = CompleterFuncBase | CompleterFuncWithTokens class ChoicesCallable: @@ -437,7 +435,7 @@ class ChoicesCallable: def __init__( self, is_completer: bool, - to_call: Union[CompleterFunc, ChoicesProviderFunc], + to_call: CompleterFunc | ChoicesProviderFunc, ) -> None: """Initialize the ChoiceCallable instance. @@ -498,7 +496,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. @@ -508,7 +506,7 @@ def _action_get_choices_callable(self: argparse.Action) -> Optional[ChoicesCalla :param self: argparse Action being queried :return: A ChoicesCallable instance or None if attribute does not exist """ - return cast(Optional[ChoicesCallable], getattr(self, ATTR_CHOICES_CALLABLE, None)) + return cast(ChoicesCallable | None, getattr(self, ATTR_CHOICES_CALLABLE, None)) setattr(argparse.Action, 'get_choices_callable', _action_get_choices_callable) @@ -584,7 +582,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. @@ -594,13 +592,13 @@ def _action_get_descriptive_header(self: argparse.Action) -> Optional[str]: :param self: argparse Action being queried :return: The value of descriptive_header or None if attribute does not exist """ - return cast(Optional[str], getattr(self, ATTR_DESCRIPTIVE_HEADER, None)) + return cast(str | None, getattr(self, ATTR_DESCRIPTIVE_HEADER, None)) 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. @@ -619,7 +617,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. @@ -629,13 +627,13 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[ :param self: argparse Action being queried :return: The value of nargs_range or None if attribute does not exist """ - return cast(Optional[tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None)) + return cast(tuple[int, int | float] | None, getattr(self, ATTR_NARGS_RANGE, None)) setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range) -def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[tuple[int, Union[int, float]]]) -> None: +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. @@ -694,7 +692,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. @@ -760,11 +758,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. @@ -810,7 +808,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 @@ -951,7 +949,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. @@ -961,7 +959,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Opti :param self: ArgumentParser being queried :return: An ArgparseCompleter-based class or None if attribute does not exist """ - return cast(Optional[type['ArgparseCompleter']], getattr(self, ATTR_AP_COMPLETER_TYPE, None)) + return cast(type['ArgparseCompleter'] | None, getattr(self, ATTR_AP_COMPLETER_TYPE, None)) setattr(argparse.ArgumentParser, 'get_ap_completer_type', _ArgumentParser_get_ap_completer_type) @@ -1077,9 +1075,9 @@ def __init__( prog: str, indent_increment: int = 2, max_help_position: int = 24, - width: Optional[int] = None, + width: int | None = None, *, - console: Optional[rich_utils.Cmd2Console] = None, + console: rich_utils.Cmd2Console | None = None, **kwargs: Any, ) -> None: """Initialize Cmd2HelpFormatter.""" @@ -1090,10 +1088,10 @@ def __init__( 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: ') @@ -1147,7 +1145,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 @@ -1227,8 +1225,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 @@ -1244,7 +1242,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) @@ -1255,7 +1253,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) @@ -1361,15 +1359,15 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): def __init__( self, - prog: Optional[str] = None, - usage: Optional[str] = None, - description: Optional[RenderableType] = None, - epilog: Optional[RenderableType] = None, + prog: str | None = None, + usage: str | None = None, + description: RenderableType | None = None, + epilog: RenderableType | None = None, parents: Sequence[argparse.ArgumentParser] = (), formatter_class: type[Cmd2HelpFormatter] = 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, @@ -1377,7 +1375,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. @@ -1411,8 +1409,8 @@ def __init__( ) # Recast to assist type checkers since these can be Rich renderables in a Cmd2HelpFormatter. - self.description: Optional[RenderableType] = self.description # type: ignore[assignment] - self.epilog: Optional[RenderableType] = self.epilog # type: ignore[assignment] + self.description: RenderableType | None = self.description # type: ignore[assignment] + self.epilog: RenderableType | None = self.epilog # type: ignore[assignment] self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined] @@ -1473,10 +1471,7 @@ def format_help(self) -> str: # positionals, optionals and user-defined groups for action_group in self._action_groups: - if sys.version_info >= (3, 10): - default_options_group = action_group.title == 'options' - else: - default_options_group = action_group.title == 'optional arguments' + default_options_group = action_group.title == 'options' if default_options_group: # check if the arguments are required, group accordingly diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index da308ef4..1f9e5531 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -57,7 +57,6 @@ TYPE_CHECKING, Any, ClassVar, - Optional, TextIO, TypeVar, Union, @@ -188,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: @@ -196,7 +195,7 @@ 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] = [] @@ -206,7 +205,7 @@ def __init__(self) -> None: if TYPE_CHECKING: # pragma: no cover StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] - ClassArgParseBuilder = classmethod[Union['Cmd', CommandSet], [], argparse.ArgumentParser] + ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] else: StaticArgParseBuilder = staticmethod ClassArgParseBuilder = classmethod @@ -242,7 +241,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. @@ -299,8 +298,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, @@ -309,12 +308,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, @@ -458,7 +457,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 @@ -494,7 +493,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: @@ -623,7 +622,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. @@ -640,7 +639,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 @@ -751,12 +750,10 @@ def register_command_set(self, cmdset: CommandSet) -> None: def _build_parser( self, parent: CommandParent, - parser_builder: Union[ - argparse.ArgumentParser, - Callable[[], argparse.ArgumentParser], - StaticArgParseBuilder, - ClassArgParseBuilder, - ], + parser_builder: argparse.ArgumentParser + | Callable[[], argparse.ArgumentParser] + | StaticArgParseBuilder + | ClassArgParseBuilder, prog: str, ) -> argparse.ArgumentParser: """Build argument parser for a command/subcommand. @@ -1191,9 +1188,9 @@ def print_to( *objects: Any, sep: str = " ", end: str = "\n", - style: Optional[StyleType] = None, - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + style: StyleType | None = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to a given file stream. @@ -1238,9 +1235,9 @@ def poutput( *objects: Any, sep: str = " ", end: str = "\n", - style: Optional[StyleType] = None, - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + style: StyleType | None = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to self.stdout. @@ -1274,9 +1271,9 @@ def perror( *objects: Any, sep: str = " ", end: str = "\n", - style: Optional[StyleType] = "cmd2.error", - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + style: StyleType | None = "cmd2.error", + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print objects to sys.stderr. @@ -1310,8 +1307,8 @@ def psuccess( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Wrap poutput, but apply cmd2.success style. @@ -1343,8 +1340,8 @@ def pwarning( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Wrap perror, but apply cmd2.warning style. @@ -1375,7 +1372,7 @@ def pexcept( self, exception: BaseException, end: str = "\n", - rich_print_kwargs: Optional[RichPrintKwargs] = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print exception to sys.stderr. If debug is true, print exception traceback if one exists. @@ -1411,9 +1408,9 @@ def pfeedback( *objects: Any, sep: str = " ", end: str = "\n", - style: Optional[StyleType] = None, - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + style: StyleType | None = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """For printing nonessential feedback. Can be silenced with `quiet`. @@ -1459,10 +1456,10 @@ def ppaged( *objects: Any, sep: str = " ", end: str = "\n", - style: Optional[StyleType] = None, + style: StyleType | None = None, chop: bool = False, - soft_wrap: Optional[bool] = None, - rich_print_kwargs: Optional[RichPrintKwargs] = None, + soft_wrap: bool | None = None, + rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print output using a pager if it would go off screen and stdout isn't currently being redirected. @@ -1726,9 +1723,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. @@ -1775,9 +1772,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. @@ -1805,7 +1802,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 @@ -1825,7 +1822,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. @@ -2139,7 +2136,7 @@ def _display_matches_gnu_readline( # rl_display_match_list() expects matches to be in argv format where # substitution is the first element, followed by the matches, and then a NULL. - strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) + strings_array = cast(list[bytes | None], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))()) # Copy in the encoded strings and add a NULL to the end strings_array[0] = encoded_substitution @@ -2189,11 +2186,10 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar """Determine what type of ArgparseCompleter to use on a given parser. If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER. - :param parser: the parser to examine :return: type of ArgparseCompleter """ - Completer = Optional[type[argparse_completer.ArgparseCompleter]] # noqa: N806 + Completer = type[argparse_completer.ArgparseCompleter] | None # noqa: N806 completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined] if completer_type is None: @@ -2201,7 +2197,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(). @@ -2379,8 +2375,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, …, @@ -2579,7 +2575,7 @@ def get_help_topics(self) -> list[str]: def sigint_handler( self, signum: int, # noqa: ARG002, - frame: Optional[FrameType], # noqa: ARG002, + frame: FrameType | None, # noqa: ARG002, ) -> None: """Signal handler for SIGINTs which typically come from Ctrl-C events. @@ -2602,7 +2598,7 @@ def sigint_handler( 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 @@ -2622,7 +2618,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 @@ -2633,7 +2629,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 @@ -2682,7 +2678,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. @@ -2723,7 +2719,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 @@ -2805,7 +2801,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(): @@ -2825,7 +2821,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, @@ -2860,7 +2856,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 @@ -2950,7 +2946,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 @@ -3003,7 +2999,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 @@ -3061,7 +3057,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 @@ -3195,7 +3191,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 @@ -3212,7 +3208,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. @@ -3247,7 +3243,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 @@ -3265,20 +3261,20 @@ def default(self, statement: Statement) -> Optional[bool]: # type: ignore[overr self.perror(err_msg, style=None) 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. @@ -3312,8 +3308,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.""" @@ -3332,7 +3328,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 @@ -4105,7 +4101,7 @@ def do_help(self, args: argparse.Namespace) -> None: self.perror(err_msg, style=None) 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. @@ -4123,7 +4119,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. @@ -4260,7 +4256,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 # Non-argparse commands can have help_functions for their documentation if command in topics: @@ -4316,7 +4312,7 @@ def _build_eof_parser() -> Cmd2ArgumentParser: return eof_parser @with_argparser(_build_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. @@ -4331,13 +4327,13 @@ def _build_quit_parser() -> Cmd2ArgumentParser: return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application.") @with_argparser(_build_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. @@ -4350,12 +4346,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()))) + local_opts = cast(list[tuple[Any, str | None]], list(zip(opts.split(), opts.split(), strict=False))) 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)) @@ -4695,7 +4691,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(). @@ -4818,7 +4814,7 @@ def _build_py_parser() -> Cmd2ArgumentParser: return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell.") @with_argparser(_build_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. @@ -4839,7 +4835,7 @@ def _build_run_pyscript_parser(cls) -> Cmd2ArgumentParser: return run_pyscript_parser @with_argparser(_build_run_pyscript_parser) - def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]: + def do_run_pyscript(self, args: argparse.Namespace) -> bool | None: """Run Python script within this application's environment. :return: True if running of commands should stop @@ -4877,7 +4873,7 @@ def _build_ipython_parser() -> Cmd2ArgumentParser: return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell.") @with_argparser(_build_ipython_parser) - def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover + def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover """Run an interactive IPython shell. :return: True if running of commands should stop @@ -5012,7 +5008,7 @@ def _build_history_parser(cls) -> Cmd2ArgumentParser: return history_parser @with_argparser(_build_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 @@ -5241,7 +5237,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, @@ -5360,7 +5356,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. @@ -5376,7 +5372,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] @@ -5413,7 +5409,7 @@ def _build_run_script_parser(cls) -> Cmd2ArgumentParser: return run_script_parser @with_argparser(_build_run_script_parser) - def do_run_script(self, args: argparse.Namespace) -> Optional[bool]: + def do_run_script(self, args: argparse.Namespace) -> bool | None: """Run text script. :return: True if running of commands should stop @@ -5497,7 +5493,7 @@ def _build_relative_run_script_parser(cls) -> Cmd2ArgumentParser: return relative_run_script_parser @with_argparser(_build_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 text script. This command is intended to be used from within a text script. @@ -5573,7 +5569,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 @@ -5815,7 +5811,7 @@ def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_ """ self.perror(message_to_print, style=None) - 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 @@ -6007,7 +6003,7 @@ def _resolve_func_self( self, cmd_support_func: Callable[..., Any], cmd_self: Union[CommandSet, 'Cmd', None], - ) -> Optional[object]: + ) -> 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. @@ -6019,7 +6015,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): @@ -6030,7 +6026,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..3bf6afc8 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -3,7 +3,6 @@ from collections.abc import Callable, Mapping from typing import ( TYPE_CHECKING, - Optional, TypeVar, ) @@ -23,7 +22,7 @@ #: Callable signature for a basic command function #: Further refinements are needed to define the input parameters -CommandFunc = Callable[..., Optional[bool]] +CommandFunc = Callable[..., bool | None] CommandSetType = TypeVar('CommandSetType', bound=type['CommandSet']) @@ -91,7 +90,7 @@ 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__ diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 246055fa..cae1b399 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -5,7 +5,6 @@ from typing import ( TYPE_CHECKING, Any, - Optional, TypeVar, Union, ) @@ -62,10 +61,10 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet]) -CommandParentType = TypeVar('CommandParentType', bound=Union[type['cmd2.Cmd'], type[CommandSet]]) +CommandParentType = TypeVar('CommandParentType', bound=type['cmd2.Cmd'] | type[CommandSet]) -RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Union[Statement, str]], Optional[bool]] +RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Statement | str], bool | None] ########################## @@ -73,7 +72,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 +97,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 @@ -114,7 +113,7 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> #: Function signature for a command function that accepts a pre-processed argument list from user input #: and optionally returns a boolean -ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, list[str]], Optional[bool]] +ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, list[str]], bool | None] #: Function signature for a command function that accepts a pre-processed argument list from user input #: and returns a boolean ArgListCommandFuncBoolReturn = Callable[[CommandParent, list[str]], bool] @@ -123,21 +122,21 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> ArgListCommandFuncNoneReturn = Callable[[CommandParent, list[str]], None] #: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list -ArgListCommandFunc = Union[ - ArgListCommandFuncOptionalBoolReturn[CommandParent], - ArgListCommandFuncBoolReturn[CommandParent], - ArgListCommandFuncNoneReturn[CommandParent], -] +ArgListCommandFunc = ( + ArgListCommandFuncOptionalBoolReturn[CommandParent] + | ArgListCommandFuncBoolReturn[CommandParent] + | ArgListCommandFuncNoneReturn[CommandParent] +) 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 +168,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: @@ -194,8 +193,8 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and optionally return a boolean -ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]] -ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], Optional[bool]] +ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], bool | None] +ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], bool | None] #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and return a boolean @@ -208,30 +207,28 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]: ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, list[str]], None] #: Aggregate of all accepted function signatures for an argparse command function -ArgparseCommandFunc = Union[ - ArgparseCommandFuncOptionalBoolReturn[CommandParent], - ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CommandParent], - ArgparseCommandFuncBoolReturn[CommandParent], - ArgparseCommandFuncWithUnknownArgsBoolReturn[CommandParent], - ArgparseCommandFuncNoneReturn[CommandParent], - ArgparseCommandFuncWithUnknownArgsNoneReturn[CommandParent], -] +ArgparseCommandFunc = ( + ArgparseCommandFuncOptionalBoolReturn[CommandParent] + | ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CommandParent] + | ArgparseCommandFuncBoolReturn[CommandParent] + | ArgparseCommandFuncWithUnknownArgsBoolReturn[CommandParent] + | ArgparseCommandFuncNoneReturn[CommandParent] + | ArgparseCommandFuncWithUnknownArgsNoneReturn[CommandParent] +) 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 # existing parser + | Callable[[], argparse.ArgumentParser] # function or staticmethod + | Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod *, - 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]]: """Decorate a ``do_*`` method to populate its ``args`` argument with the given instance of argparse.ArgumentParser. - :param parser: unique instance of ArgumentParser or a callable that returns an ArgumentParser + :param parser: instance of ArgumentParser or a callable that returns an ArgumentParser for this command :param ns_provider: An optional function that accepts a cmd2.Cmd or cmd2.CommandSet object as an argument and returns an argparse.Namespace. This is useful if the Namespace needs to be prepopulated with state data that affects parsing. @@ -286,7 +283,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: @@ -317,7 +314,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: @@ -355,20 +352,18 @@ 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 # existing parser + | Callable[[], argparse.ArgumentParser] # function or staticmethod + | Callable[[CommandParentType], argparse.ArgumentParser], # Cmd or CommandSet classmethod *, - 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. :param command: Command Name. Space-delimited subcommands may optionally be specified :param subcommand: Subcommand name - :param parser: argparse Parser for this subcommand + :param parser: instance of ArgumentParser or a callable that returns an ArgumentParser for this subcommand :param help: Help message for this subcommand which displays in the list of subcommands of the command we are adding to. This is passed as the help argument to subparsers.add_parser(). :param aliases: Alternative names for this subcommand. This is passed as the alias argument to diff --git a/cmd2/history.py b/cmd2/history.py index 1a8582b6..6124c30c 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -11,8 +11,6 @@ ) from typing import ( Any, - Optional, - Union, overload, ) @@ -164,7 +162,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 +175,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 @@ -332,7 +330,7 @@ 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 + self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None ) -> 'OrderedDict[int, HistoryItem]': """Build history search results. diff --git a/cmd2/parsing.py b/cmd2/parsing.py index e12f799c..75e6fa41 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -9,8 +9,6 @@ ) from typing import ( Any, - Optional, - Union, ) from . import ( @@ -250,10 +248,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 +583,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..9243d232 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -3,7 +3,6 @@ from dataclasses import ( dataclass, ) -from typing import Optional from .parsing import ( Statement, @@ -38,4 +37,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 fe340523..3cd08494 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -10,9 +10,7 @@ TYPE_CHECKING, Any, NamedTuple, - Optional, TextIO, - Union, cast, ) @@ -98,7 +96,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') @@ -114,7 +112,7 @@ def __call__(self, command: str, *, echo: Optional[bool] = None) -> CommandResul stdouts_match = self._cmd2_app.stdout == sys.stdout # This will be used to capture _cmd2_app.stdout and sys.stdout - copy_cmd_stdout = StdSim(cast(Union[TextIO, StdSim], self._cmd2_app.stdout), echo=echo) + copy_cmd_stdout = StdSim(cast(TextIO | StdSim, self._cmd2_app.stdout), echo=echo) # Pause the storing of stdout until onecmd_plus_hooks enables it copy_cmd_stdout.pause_storage = True diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 623dc935..44e4ee29 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -5,7 +5,6 @@ from typing import ( IO, Any, - Optional, TypedDict, ) @@ -61,7 +60,7 @@ def __repr__(self) -> str: class Cmd2Theme(Theme): """Rich theme class used by Cmd2Console.""" - def __init__(self, styles: Optional[Mapping[str, StyleType]] = None, inherit: bool = True) -> None: + def __init__(self, styles: Mapping[str, StyleType] | None = None, inherit: bool = True) -> None: """Cmd2Theme initializer. :param styles: optional mapping of style names on to styles. @@ -107,14 +106,14 @@ class RichPrintKwargs(TypedDict, total=False): dictionary, Rich's default behavior for that argument will apply. """ - justify: Optional[JustifyMethod] - overflow: Optional[OverflowMethod] - no_wrap: Optional[bool] - markup: Optional[bool] - emoji: Optional[bool] - highlight: Optional[bool] - width: Optional[int] - height: Optional[int] + justify: JustifyMethod | None + overflow: OverflowMethod | None + no_wrap: bool | None + markup: bool | None + emoji: bool | None + highlight: bool | None + width: int | None + height: int | None crop: bool new_line_start: bool diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 137d447e..b6ae824c 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -5,7 +5,6 @@ from enum import ( Enum, ) -from typing import Union ######################################################################################################################### # NOTE ON LIBEDIT: @@ -191,7 +190,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..df1a722b 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -16,7 +16,6 @@ ) from typing import ( Any, - Optional, ) from wcwidth import ( # type: ignore[import] @@ -57,7 +56,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 +542,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 +736,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 +1034,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/utils.py b/cmd2/utils.py index fac5f07f..4327627b 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -16,7 +16,7 @@ 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, Union, cast, get_type_hints from . import constants from .argparse_custom import ChoicesProviderFunc, CompleterFunc @@ -95,15 +95,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. @@ -238,7 +238,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 @@ -250,7 +250,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'] @@ -328,7 +328,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. @@ -467,7 +467,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 @@ -549,7 +549,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 @@ -631,7 +631,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 @@ -680,9 +680,9 @@ class RedirectionSavedState: def __init__( self, - self_stdout: Union[StdSim, TextIO], + self_stdout: StdSim | TextIO, stdouts_match: bool, - pipe_proc_reader: Optional[ProcReader], + pipe_proc_reader: ProcReader | None, saved_redirecting: bool, ) -> None: """RedirectionSavedState initializer. @@ -728,14 +728,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() @@ -798,7 +798,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: @@ -920,7 +920,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. @@ -943,7 +943,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. @@ -966,7 +966,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. @@ -1095,7 +1095,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 @@ -1126,7 +1126,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: @@ -1223,8 +1223,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/docs/overview/installation.md b/docs/overview/installation.md index 0f94f06b..e3c12d60 100644 --- a/docs/overview/installation.md +++ b/docs/overview/installation.md @@ -1,6 +1,6 @@ # Installation Instructions -`cmd2` works on Linux, macOS, and Windows. It requires Python 3.9 or higher, +`cmd2` works on Linux, macOS, and Windows. It requires Python 10 or higher, [pip](https://pypi.org/project/pip), and [setuptools](https://pypi.org/project/setuptools). If you've got all that, then you can just: @@ -18,7 +18,7 @@ $ pip install cmd2 ## Prerequisites -If you have Python 3 >=3.9 installed from [python.org](https://www.python.org), you will already +If you have Python 3 >=3.10 installed from [python.org](https://www.python.org), you will already have [pip](https://pypi.org/project/pip) and [setuptools](https://pypi.org/project/setuptools), but may need to upgrade to the latest versions: diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py index f03ea38d..2fba205e 100755 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands_main.py @@ -5,7 +5,6 @@ import argparse from collections.abc import Iterable -from typing import Optional from modular_commands.commandset_basic import ( # noqa: F401 BasicCompletionCommandSet, @@ -26,7 +25,7 @@ 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/package.json b/package.json index 6d5accd8..f3aa74f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "prettier": "^3.5.3", - "prettier-plugin-toml": "^2.0.5" + "prettier": "^3.6.2", + "prettier-plugin-toml": "^2.0.6" } } diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh index 4b515bbf..9ee27578 100644 --- a/plugins/ext_test/build-pyenvs.sh +++ b/plugins/ext_test/build-pyenvs.sh @@ -8,7 +8,7 @@ # version numbers are: major.minor.patch # # this script will delete and recreate existing virtualenvs named -# cmd2-3.9, etc. It will also create a .python-version +# cmd2-3.14, etc. It will also create a .python-version # # Prerequisites: # - *nix-ish environment like macOS or Linux @@ -23,7 +23,7 @@ # virtualenvs will be added to '.python-version'. Feel free to modify # this list, but note that this script intentionally won't install # dev, rc, or beta python releases -declare -a pythons=("3.9", "3.10", "3.11", "3.12", "3.13") +declare -a pythons=("3.10", "3.11", "3.12", "3.13", "3.14") # function to find the latest patch of a minor version of python function find_latest_version { 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..843d609f 100644 --- a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py +++ b/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py @@ -2,7 +2,6 @@ from typing import ( TYPE_CHECKING, - Optional, ) import cmd2 @@ -29,7 +28,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 | None = None) -> cmd2.CommandResult: """ Run the application command diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py index d8aa344b..9a29eaab 100644 --- a/plugins/ext_test/noxfile.py +++ b/plugins/ext_test/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13']) +@nox.session(python=['3.10', '3.11', '3.12', '3.13', '3.14']) def tests(session): session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index e3b38776..244b85cf 100644 --- a/plugins/ext_test/setup.py +++ b/plugins/ext_test/setup.py @@ -24,7 +24,7 @@ license='MIT', package_data=PACKAGE_DATA, packages=['cmd2_ext_test'], - python_requires='>=3.9', + python_requires='>=3.10', install_requires=['cmd2 >= 2, <3'], setup_requires=['setuptools >= 42', 'setuptools_scm >= 3.4'], classifiers=[ @@ -34,7 +34,6 @@ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', diff --git a/plugins/template/README.md b/plugins/template/README.md index 11fe2690..4ade388f 100644 --- a/plugins/template/README.md +++ b/plugins/template/README.md @@ -215,28 +215,24 @@ If you prefer to create these virtualenvs by hand, do the following: ``` $ cd cmd2_abbrev -$ pyenv install 3.8.5 -$ pyenv virtualenv -p python3.8 3.8.5 cmd2-3.8 -$ pyenv install 3.9.0 -$ pyenv virtualenv -p python3.9 3.9.0 cmd2-3.9 +$ pyenv install 3.14.0 +$ pyenv virtualenv -p python3.14 3.14.0 cmd2-3.14 ``` Now set pyenv to make both of those available at the same time: ``` -$ pyenv local cmd2-3.8 cmd2-3.9 +$ pyenv local cmd2-3.14 ``` Whether you ran the script, or did it by hand, you now have isolated virtualenvs for each of the major python versions. This table shows various python commands, the version of python which will be executed, and the virtualenv it will utilize. -| Command | python | virtualenv | -| ----------- | ------ | ---------- | -| `python3.8` | 3.8.5 | cmd2-3.8 | -| `python3.9` | 3.9.0 | cmd2-3.9 | -| `pip3.8` | 3.8.5 | cmd2-3.8 | -| `pip3.9` | 3.9.0 | cmd2-3.9 | +| Command | python | virtualenv | +| ------------ | ------ | ---------- | +| `python3.14` | 3.14.0 | cmd2-3.14 | +| `pip3.14` | 3.14.0 | cmd2-3.14 | ## Install Dependencies @@ -249,10 +245,10 @@ $ pip install -e .[dev] This command also installs `cmd2-myplugin` "in-place", so the package points to the source code instead of copying files to the python `site-packages` folder. -All the dependencies now have been installed in the `cmd2-3.9` virtualenv. If you want to work in +All the dependencies now have been installed in the `cmd2-3.14` virtualenv. If you want to work in other virtualenvs, you'll need to manually select it, and install again:: -$ pyenv shell cmd2-3.4 $ pip install -e .[dev] +$ pyenv shell cmd2-3.14 $ pip install -e .[dev] Now that you have your python environments created, you need to install the package in place, along with all the other development dependencies: @@ -268,8 +264,8 @@ the `tests` directory. ### Use nox to run unit tests in multiple versions of python -The included `noxfile.py` is setup to run the unit tests in python 3.8, 3.9 3.10, 3.11, and 3.12 You -can run your unit tests in all of these versions of python by: +The included `noxfile.py` is setup to run the unit tests in 3.10, 3.11, 3.12, 3.13, and 3.14 You can +run your unit tests in all of these versions of python by: ``` $ nox diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh index fd0b505b..9ee27578 100644 --- a/plugins/template/build-pyenvs.sh +++ b/plugins/template/build-pyenvs.sh @@ -8,7 +8,7 @@ # version numbers are: major.minor.patch # # this script will delete and recreate existing virtualenvs named -# cmd2-3.9, etc. It will also create a .python-version +# cmd2-3.14, etc. It will also create a .python-version # # Prerequisites: # - *nix-ish environment like macOS or Linux @@ -23,7 +23,7 @@ # virtualenvs will be added to '.python-version'. Feel free to modify # this list, but note that this script intentionally won't install # dev, rc, or beta python releases -declare -a pythons=("3.9" "3.10" "3.11", "3.12", "3.13") +declare -a pythons=("3.10", "3.11", "3.12", "3.13", "3.14") # function to find the latest patch of a minor version of python function find_latest_version { diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py index cac9f917..d37ed138 100644 --- a/plugins/template/noxfile.py +++ b/plugins/template/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13']) +@nox.session(python=['3.10', '3.11', '3.12', '3.13', '3.14']) def tests(session) -> None: session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/template/setup.py b/plugins/template/setup.py index 3eed7f28..cc4f6331 100644 --- a/plugins/template/setup.py +++ b/plugins/template/setup.py @@ -20,7 +20,7 @@ url='https://github.com/python-cmd2/cmd2-plugin-template', license='MIT', packages=['cmd2_myplugin'], - python_requires='>=3.9', + python_requires='>=3.10', install_requires=['cmd2 >= 2, <3'], setup_requires=['setuptools_scm'], classifiers=[ @@ -30,7 +30,6 @@ 'Topic :: Software Development :: Libraries :: Python Modules', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', diff --git a/pyproject.toml b/pyproject.toml index 9f3990d0..cce63b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python" authors = [{ name = "cmd2 Contributors" }] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = ["CLI", "cmd", "command", "interactive", "prompt", "Python"] license = { file = "LICENSE" } classifiers = [ @@ -19,7 +19,6 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -149,7 +148,7 @@ exclude = [ # Same as Black. line-length = 127 indent-width = 4 -target-version = "py39" # Minimum supported version of Python +target-version = "py310" # Minimum supported version of Python output-format = "full" [tool.ruff.lint] @@ -225,7 +224,6 @@ select = [ ignore = [ # `uv run ruff rule E501` for a description of that rule "ANN401", # Dynamically typed expressions (typing.Any) are disallowed (would be good to enable this later) - "B905", # zip() without an explicit strict= parameter (strict added in Python 3.10+) "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) "COM819", # Conflicts with ruff format "D203", # 1 blank line required before class docstring (conflicts with D211) @@ -243,7 +241,6 @@ 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+) "UP038", # Use X | Y in {} call instead of (X, Y) - deprecated due to poor performance (requires Python 3.10+) "W191", # Conflicts with ruff format diff --git a/tasks.py b/tasks.py index f6b9d7ff..92917dba 100644 --- a/tasks.py +++ b/tasks.py @@ -12,7 +12,6 @@ import re import shutil import sys -from typing import Union import invoke from invoke.context import Context @@ -26,7 +25,7 @@ # 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 35bb90e6..df5159a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,10 +3,6 @@ import argparse import sys from contextlib import redirect_stderr -from typing import ( - Optional, - Union, -) from unittest import ( mock, ) @@ -22,9 +18,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 @@ -148,7 +142,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/test_argparse.py b/tests/test_argparse.py index 7eb32076..afcae62e 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -1,7 +1,6 @@ """Cmd2 testing for argument parsing""" import argparse -from typing import Optional import pytest @@ -32,7 +31,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. :param args: argparse namespace @@ -70,7 +69,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: @@ -92,7 +91,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_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index fe6af8c3..c2bdf81f 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -5,10 +5,6 @@ redirect_stderr, redirect_stdout, ) -from typing import ( - Optional, - Union, -) from unittest import ( mock, ) @@ -27,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 @@ -134,7 +128,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