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