From 5c771da6750fc095a1a77ba32fe4dc167f9992a3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 16:34:00 -0500 Subject: [PATCH 1/7] Fixed custom types and moved common ones into types.py. --- cmd2/argparse_custom.py | 27 +++++++-------- cmd2/cmd2.py | 24 ++++++++------ cmd2/command_definition.py | 8 ++--- cmd2/completion.py | 54 ------------------------------ cmd2/decorators.py | 68 ++++++++++++++++++-------------------- cmd2/py_bridge.py | 4 +-- cmd2/types.py | 68 ++++++++++++++++++++++++++++++++++++++ cmd2/utils.py | 13 ++++---- 8 files changed, 140 insertions(+), 126 deletions(-) create mode 100644 cmd2/types.py diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index d3ea4e8c9..2830a45a1 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -294,13 +294,14 @@ def get_choices(self) -> Choices: from . import constants from . import rich_utils as ru -from .completion import ( +from .completion import CompletionItem +from .rich_utils import Cmd2RichArgparseConsole +from .styles import Cmd2Style +from .types import ( ChoicesProviderUnbound, + CmdOrSet, CompleterUnbound, - CompletionItem, ) -from .rich_utils import Cmd2RichArgparseConsole -from .styles import Cmd2Style if TYPE_CHECKING: # pragma: no cover from .argparse_completer import ArgparseCompleter @@ -384,7 +385,7 @@ class ChoicesCallable: def __init__( self, is_completer: bool, - to_call: ChoicesProviderUnbound | CompleterUnbound, + to_call: ChoicesProviderUnbound[CmdOrSet] | CompleterUnbound[CmdOrSet], ) -> None: """Initialize the ChoiceCallable instance. @@ -396,18 +397,18 @@ def __init__( self.to_call = to_call @property - def choices_provider(self) -> ChoicesProviderUnbound: + def choices_provider(self) -> ChoicesProviderUnbound[CmdOrSet]: """Retreive the internal choices_provider function.""" if self.is_completer: raise AttributeError("This instance is configured as a completer, not a choices_provider") - return cast(ChoicesProviderUnbound, self.to_call) + return cast(ChoicesProviderUnbound[CmdOrSet], self.to_call) @property - def completer(self) -> CompleterUnbound: + def completer(self) -> CompleterUnbound[CmdOrSet]: """Retreive the internal completer function.""" if not self.is_completer: raise AttributeError("This instance is configured as a choices_provider, not a completer") - return cast(CompleterUnbound, self.to_call) + return cast(CompleterUnbound[CmdOrSet], self.to_call) ############################################################################################################ @@ -476,7 +477,7 @@ def _action_set_choices_callable(self: argparse.Action, choices_callable: Choice def _action_set_choices_provider( self: argparse.Action, - choices_provider: ChoicesProviderUnbound, + choices_provider: ChoicesProviderUnbound[CmdOrSet], ) -> None: """Set choices_provider of an argparse Action. @@ -496,7 +497,7 @@ def _action_set_choices_provider( def _action_set_completer( self: argparse.Action, - completer: CompleterUnbound, + completer: CompleterUnbound[CmdOrSet], ) -> None: """Set completer of an argparse Action. @@ -694,8 +695,8 @@ def _add_argument_wrapper( self: argparse._ActionsContainer, *args: Any, nargs: int | str | tuple[int] | tuple[int, int] | tuple[int, float] | None = None, - choices_provider: ChoicesProviderUnbound | None = None, - completer: CompleterUnbound | None = None, + choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None, + completer: CompleterUnbound[CmdOrSet] | None = None, suppress_tab_hint: bool = False, table_header: Sequence[str | Column] | None = None, **kwargs: Any, diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 4f36c3f03..8fff41a60 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -107,12 +107,8 @@ ) from .completion import ( Choices, - ChoicesProviderUnbound, - CompleterBound, - CompleterUnbound, CompletionItem, Completions, - Matchable, ) from .constants import ( CLASS_ATTR_DEFAULT_HELP_CATEGORY, @@ -121,7 +117,6 @@ HELP_FUNC_PREFIX, ) from .decorators import ( - CommandParent, as_subcommand_to, with_argparser, ) @@ -152,6 +147,13 @@ RichPrintKwargs, ) from .styles import Cmd2Style +from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterBound, + CompleterUnbound, + Matchable, +) with contextlib.suppress(ImportError): from IPython import start_ipython @@ -840,7 +842,7 @@ def register_command_set(self, cmdset: CommandSet) -> None: def _build_parser( self, - parent: CommandParent, + parent: CmdOrSet, parser_builder: argparse.ArgumentParser | Callable[[], argparse.ArgumentParser] | StaticArgParseBuilder @@ -849,7 +851,7 @@ def _build_parser( ) -> argparse.ArgumentParser: """Build argument parser for a command/subcommand. - :param parent: CommandParent object which owns the command using the parser. + :param parent: object which owns the command using the parser. When parser_builder is a classmethod, this function passes parent's class to it. :param parser_builder: means used to build the parser @@ -3283,8 +3285,8 @@ def _resolve_completer( self, preserve_quotes: bool = False, choices: Iterable[Any] | None = None, - choices_provider: ChoicesProviderUnbound | None = None, - completer: CompleterUnbound | None = None, + choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None, + completer: CompleterUnbound[CmdOrSet] | None = None, parser: argparse.ArgumentParser | None = None, ) -> Completer: """Determine the appropriate completer based on provided arguments.""" @@ -3315,8 +3317,8 @@ def read_input( history: Sequence[str] | None = None, preserve_quotes: bool = False, choices: Iterable[Any] | None = None, - choices_provider: ChoicesProviderUnbound | None = None, - completer: CompleterUnbound | None = None, + choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None, + completer: CompleterUnbound[CmdOrSet] | None = None, parser: argparse.ArgumentParser | None = None, ) -> str: """Read a line of input with optional completion and history. diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index 769d80d1c..f98ab22f5 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -18,7 +18,7 @@ from .utils import Settable if TYPE_CHECKING: # pragma: no cover - import cmd2 + from .cmd2 import Cmd #: Callable signature for a basic command function #: Further refinements are needed to define the input parameters @@ -92,13 +92,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: cmd2.Cmd | None = None + self.__cmd_internal: Cmd | None = None self._settables: dict[str, Settable] = {} self._settable_prefix = self.__class__.__name__ @property - def _cmd(self) -> 'cmd2.Cmd': + def _cmd(self) -> 'Cmd': """Property for child classes to access self.__cmd_internal. Using this property ensures that self.__cmd_internal has been set @@ -122,7 +122,7 @@ def _cmd(self) -> CustomCmdApp: raise CommandSetRegistrationError('This CommandSet is not registered') return self.__cmd_internal - def on_register(self, cmd: 'cmd2.Cmd') -> None: + def on_register(self, cmd: '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/completion.py b/cmd2/completion.py index 3664be2f4..91cd1f7f9 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -3,11 +3,9 @@ import re import sys from collections.abc import ( - Callable, Collection, Iterable, Iterator, - Mapping, Sequence, ) from dataclasses import ( @@ -15,19 +13,13 @@ field, ) from typing import ( - TYPE_CHECKING, Any, - TypeAlias, cast, overload, ) from . import string_utils as su -if TYPE_CHECKING: # pragma: no cover - from .cmd2 import Cmd - from .command_definition import CommandSet - if sys.version_info >= (3, 11): from typing import Self else: @@ -264,49 +256,3 @@ class Completions(CompletionResultsBase): def all_display_numeric(items: Collection[CompletionItem]) -> bool: """Return True if items is non-empty and every item.display_plain value is a numeric string.""" return bool(items) and all(NUMERIC_RE.match(item.display_plain) for item in items) - - -############################################# -# choices_provider function types -############################################# - -# Represents the parsed tokens from argparse during completion -ArgTokens: TypeAlias = Mapping[str, Sequence[str]] - -# Unbound choices_provider function types used by argparse-based completion. -# These expect a Cmd or CommandSet instance as the first argument. -ChoicesProviderUnbound: TypeAlias = ( - # Basic: (self) -> Choices - Callable[["Cmd"], Choices] - | Callable[["CommandSet"], Choices] - | - # Context-aware: (self, arg_tokens) -> Choices - Callable[["Cmd", ArgTokens], Choices] - | Callable[["CommandSet", ArgTokens], Choices] -) - -############################################# -# completer function types -############################################# - -# Unbound completer function types used by argparse-based completion. -# These expect a Cmd or CommandSet instance as the first argument. -CompleterUnbound: TypeAlias = ( - # Basic: (self, text, line, begidx, endidx) -> Completions - Callable[["Cmd", str, str, int, int], Completions] - | Callable[["CommandSet", str, str, int, int], Completions] - | - # Context-aware: (self, text, line, begidx, endidx, arg_tokens) -> Completions - Callable[["Cmd", str, str, int, int, ArgTokens], Completions] - | Callable[["CommandSet", str, str, int, int, ArgTokens], Completions] -) - -# A bound completer used internally by cmd2 for basic completion logic. -# The 'self' argument is already tied to an instance and is omitted. -# Format: (text, line, begidx, endidx) -> Completions -CompleterBound: TypeAlias = Callable[[str, str, int, int], Completions] - -# Represents a type that can be matched against when completing. -# Strings are matched directly while CompletionItems are matched -# against their 'text' member. -Matchable: TypeAlias = str | CompletionItem diff --git a/cmd2/decorators.py b/cmd2/decorators.py index d7a1c5088..eb159d157 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -10,7 +10,6 @@ Any, TypeAlias, TypeVar, - Union, ) from . import constants @@ -21,9 +20,10 @@ ) from .exceptions import Cmd2ArgparseError from .parsing import Statement +from .types import CmdOrSet if TYPE_CHECKING: # pragma: no cover - import cmd2 + from .cmd2 import Cmd def with_category(category: str) -> Callable[[CommandFunc], CommandFunc]: @@ -56,10 +56,8 @@ def cat_decorator(func: CommandFunc) -> CommandFunc: return cat_decorator -CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet]) -CommandParentClass = TypeVar('CommandParentClass', bound=type['cmd2.Cmd'] | type[CommandSet]) - -RawCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CommandParent, Statement | str], bool | None] +CmdOrSetClass = TypeVar('CmdOrSetClass', bound=type['Cmd'] | type[CommandSet]) +RawCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, Statement | str], bool | None] ########################## @@ -67,7 +65,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', Statement | str]: +def _parse_positionals(args: tuple[Any, ...]) -> tuple['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. @@ -108,29 +106,29 @@ def _arg_swap(args: Sequence[Any], search_arg: Any, *replace_arg: Any) -> list[A #: Function signature for a command function that accepts a pre-processed argument list from user input #: and optionally returns a boolean -ArgListCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CommandParent, list[str]], bool | None] +ArgListCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, list[str]], bool | None] #: Function signature for a command function that accepts a pre-processed argument list from user input #: and returns a boolean -ArgListCommandFuncBoolReturn: TypeAlias = Callable[[CommandParent, list[str]], bool] +ArgListCommandFuncBoolReturn: TypeAlias = Callable[[CmdOrSet, list[str]], bool] #: Function signature for a command function that accepts a pre-processed argument list from user input #: and returns Nothing -ArgListCommandFuncNoneReturn: TypeAlias = Callable[[CommandParent, list[str]], None] +ArgListCommandFuncNoneReturn: TypeAlias = Callable[[CmdOrSet, list[str]], None] #: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list ArgListCommandFunc: TypeAlias = ( - ArgListCommandFuncOptionalBoolReturn[CommandParent] - | ArgListCommandFuncBoolReturn[CommandParent] - | ArgListCommandFuncNoneReturn[CommandParent] + ArgListCommandFuncOptionalBoolReturn[CmdOrSet] + | ArgListCommandFuncBoolReturn[CmdOrSet] + | ArgListCommandFuncNoneReturn[CmdOrSet] ) def with_argument_list( - func_arg: ArgListCommandFunc[CommandParent] | None = None, + func_arg: ArgListCommandFunc[CmdOrSet] | None = None, *, preserve_quotes: bool = False, ) -> ( - RawCommandFuncOptionalBoolReturn[CommandParent] - | Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]] + RawCommandFuncOptionalBoolReturn[CmdOrSet] + | Callable[[ArgListCommandFunc[CmdOrSet]], RawCommandFuncOptionalBoolReturn[CmdOrSet]] ): """Decorate a ``do_*`` method to alter the arguments passed to it so it is passed a list[str]. @@ -153,7 +151,7 @@ def do_echo(self, arglist): """ import functools - def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]: + def arg_decorator(func: ArgListCommandFunc[CmdOrSet]) -> RawCommandFuncOptionalBoolReturn[CmdOrSet]: """Decorate function that ingests an Argument List function and returns a raw command function. The returned function will process the raw input into an argument list to be passed to the wrapped function. @@ -188,41 +186,41 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and optionally return a boolean -ArgparseCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CommandParent, argparse.Namespace], bool | None] +ArgparseCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool | None] ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn: TypeAlias = Callable[ - [CommandParent, argparse.Namespace, list[str]], bool | None + [CmdOrSet, argparse.Namespace, list[str]], bool | None ] #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and return a boolean -ArgparseCommandFuncBoolReturn: TypeAlias = Callable[[CommandParent, argparse.Namespace], bool] -ArgparseCommandFuncWithUnknownArgsBoolReturn: TypeAlias = Callable[[CommandParent, argparse.Namespace, list[str]], bool] +ArgparseCommandFuncBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool] +ArgparseCommandFuncWithUnknownArgsBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], bool] #: Function signatures for command functions that use an argparse.ArgumentParser to process user input #: and return nothing -ArgparseCommandFuncNoneReturn: TypeAlias = Callable[[CommandParent, argparse.Namespace], None] -ArgparseCommandFuncWithUnknownArgsNoneReturn: TypeAlias = Callable[[CommandParent, argparse.Namespace, list[str]], None] +ArgparseCommandFuncNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], None] +ArgparseCommandFuncWithUnknownArgsNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], None] #: Aggregate of all accepted function signatures for an argparse command function ArgparseCommandFunc: TypeAlias = ( - ArgparseCommandFuncOptionalBoolReturn[CommandParent] - | ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CommandParent] - | ArgparseCommandFuncBoolReturn[CommandParent] - | ArgparseCommandFuncWithUnknownArgsBoolReturn[CommandParent] - | ArgparseCommandFuncNoneReturn[CommandParent] - | ArgparseCommandFuncWithUnknownArgsNoneReturn[CommandParent] + ArgparseCommandFuncOptionalBoolReturn[CmdOrSet] + | ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn[CmdOrSet] + | ArgparseCommandFuncBoolReturn[CmdOrSet] + | ArgparseCommandFuncWithUnknownArgsBoolReturn[CmdOrSet] + | ArgparseCommandFuncNoneReturn[CmdOrSet] + | ArgparseCommandFuncWithUnknownArgsNoneReturn[CmdOrSet] ) def with_argparser( parser: argparse.ArgumentParser # existing parser | Callable[[], argparse.ArgumentParser] # function or staticmethod - | Callable[[CommandParentClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod + | Callable[[CmdOrSetClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod *, ns_provider: Callable[..., argparse.Namespace] | None = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: +) -> Callable[[ArgparseCommandFunc[CmdOrSet]], RawCommandFuncOptionalBoolReturn[CmdOrSet]]: """Decorate a ``do_*`` method to populate its ``args`` argument with the given instance of argparse.ArgumentParser. :param parser: instance of ArgumentParser or a callable that returns an ArgumentParser for this command @@ -270,7 +268,7 @@ def do_argprint(self, args, unknown): """ import functools - def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]: + def arg_decorator(func: ArgparseCommandFunc[CmdOrSet]) -> RawCommandFuncOptionalBoolReturn[CmdOrSet]: """Decorate function that ingests an Argparse Command Function and returns a raw command function. The returned function will process the raw input into an argparse Namespace to be passed to the wrapped function. @@ -351,11 +349,11 @@ def as_subcommand_to( subcommand: str, parser: argparse.ArgumentParser # existing parser | Callable[[], argparse.ArgumentParser] # function or staticmethod - | Callable[[CommandParentClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod + | Callable[[CmdOrSetClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod *, help: str | None = None, # noqa: A002 aliases: Sequence[str] | None = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: +) -> Callable[[ArgparseCommandFunc[CmdOrSet]], ArgparseCommandFunc[CmdOrSet]]: """Tag this method as a subcommand to an existing argparse decorated command. :param command: Command Name. Space-delimited subcommands may optionally be specified @@ -368,7 +366,7 @@ def as_subcommand_to( :return: Wrapper function that can receive an argparse.Namespace """ - def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFunc[CommandParent]: + def arg_decorator(func: ArgparseCommandFunc[CmdOrSet]) -> ArgparseCommandFunc[CmdOrSet]: # Set some custom attributes for this command setattr(func, constants.SUBCMD_ATTR_COMMAND, command) setattr(func, constants.CMD_ATTR_ARGPARSER, parser) diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 29a77dfcb..224aa06da 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -17,7 +17,7 @@ from .utils import StdSim # namedtuple_with_defaults, if TYPE_CHECKING: # pragma: no cover - import cmd2 + from .cmd2 import Cmd class CommandResult(NamedTuple): @@ -79,7 +79,7 @@ class PyBridge: Defaults to True. """ - def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None: + def __init__(self, cmd2_app: 'Cmd', *, add_to_history: bool = True) -> None: """Initialize PyBridge instances.""" self._cmd2_app = cmd2_app self._add_to_history = add_to_history diff --git a/cmd2/types.py b/cmd2/types.py new file mode 100644 index 000000000..ab6fa8cc2 --- /dev/null +++ b/cmd2/types.py @@ -0,0 +1,68 @@ +"""Defines common types used throughout cmd2.""" + +from __future__ import annotations + +from collections.abc import ( + Callable, + Mapping, + Sequence, +) +from typing import ( + TYPE_CHECKING, + TypeAlias, + TypeVar, + Union, +) + +if TYPE_CHECKING: # pragma: no cover + from .cmd2 import Cmd + from .command_definition import CommandSet + from .completion import Choices, CompletionItem, Completions + +# A Cmd or CommandSet +CmdOrSet = TypeVar("CmdOrSet", bound=Union["Cmd", "CommandSet"]) + +################################################## +# Types used in choices_providers and completers +################################################## + +# Represents the parsed tokens from argparse during completion +ArgTokens: TypeAlias = Mapping[str, Sequence[str]] + +# Represents a type that can be matched against when completing. +# Strings are matched directly while CompletionItems are matched +# against their 'text' member. +Matchable: TypeAlias = Union[str, "CompletionItem"] + +################################################## +# choices_provider function types +################################################## + +# Unbound choices_provider function types used by argparse-based completion. +# These expect a Cmd or CommandSet instance as the first argument. +ChoicesProviderUnbound: TypeAlias = ( + # Basic: (self) -> Choices + Callable[[CmdOrSet], "Choices"] + | + # Context-aware: (self, arg_tokens) -> Choices + Callable[[CmdOrSet, "ArgTokens"], "Choices"] +) + +################################################## +# completer function types +################################################## + +# Unbound completer function types used by argparse-based completion. +# These expect a Cmd or CommandSet instance as the first argument. +CompleterUnbound: TypeAlias = ( + # Basic: (self, text, line, begidx, endidx) -> Completions + Callable[[CmdOrSet, str, str, int, int], "Completions"] + | + # Context-aware: (self, text, line, begidx, endidx, arg_tokens) -> Completions + Callable[[CmdOrSet, str, str, int, int, ArgTokens], "Completions"] +) + +# A bound completer used internally by cmd2 for basic completion logic. +# The 'self' argument is already tied to an instance and is omitted. +# Format: (text, line, begidx, endidx) -> Completions +CompleterBound: TypeAlias = Callable[[str, str, int, int], "Completions"] diff --git a/cmd2/utils.py b/cmd2/utils.py index 8d314d741..32fe17342 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -28,15 +28,14 @@ from . import constants from . import string_utils as su -from .completion import ( - Choices, +from .completion import Choices +from .types import ( ChoicesProviderUnbound, + CmdOrSet, CompleterUnbound, ) if TYPE_CHECKING: # pragma: no cover - from .decorators import CommandParent - PopenTextIO = subprocess.Popen[str] else: PopenTextIO = subprocess.Popen @@ -78,8 +77,8 @@ def __init__( settable_attrib_name: str | None = None, onchange_cb: Callable[[str, _T, _T], Any] | None = None, choices: Iterable[Any] | None = None, - choices_provider: ChoicesProviderUnbound | None = None, - completer: CompleterUnbound | None = None, + choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None, + completer: CompleterUnbound[CmdOrSet] | None = None, ) -> None: """Settable Initializer. @@ -116,7 +115,7 @@ def __init__( """ if val_type is bool: - def get_bool_choices(_cmd2_self: "CommandParent") -> Choices: + def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: """Tab complete lowercase boolean values.""" return Choices.from_values(['true', 'false']) From 9f8819210bfb0ac676958ee8a24115deaa721b67 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 16:38:52 -0500 Subject: [PATCH 2/7] Added types.py to CODEOWNERS. --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 06f90ebd3..bc74541f9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -44,6 +44,7 @@ cmd2/py_bridge.py @kmvanbrunt cmd2/rich_utils.py @kmvanbrunt cmd2/string_utils.py @kmvanbrunt cmd2/styles.py @tleonhardt @kmvanbrunt +cmd2/types.py @tleonhardt @kmvanbrunt cmd2/utils.py @tleonhardt @kmvanbrunt # Documentation From e239bb5d6ee1629541ecd0133fb3a80df77e7c27 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 17:22:09 -0500 Subject: [PATCH 3/7] Removed unnecessary import. --- cmd2/types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd2/types.py b/cmd2/types.py index ab6fa8cc2..2dfa6af95 100644 --- a/cmd2/types.py +++ b/cmd2/types.py @@ -1,7 +1,5 @@ """Defines common types used throughout cmd2.""" -from __future__ import annotations - from collections.abc import ( Callable, Mapping, From bcf046654275f5818c97e14754c4d49f395cc131 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 20:23:24 -0500 Subject: [PATCH 4/7] Added "from __future__ import annotations" to each cmd2 module. Fixed circular import between utils.py and completion.py. --- cmd2/argparse_completer.py | 20 +++++++------- cmd2/argparse_custom.py | 8 +++--- cmd2/clipboard.py | 2 ++ cmd2/cmd2.py | 55 +++++++++++++++++++------------------- cmd2/colors.py | 2 ++ cmd2/command_definition.py | 8 +++--- cmd2/completion.py | 20 +++++++++----- cmd2/constants.py | 9 ++++--- cmd2/decorators.py | 4 ++- cmd2/exceptions.py | 2 ++ cmd2/history.py | 25 ++++++++++------- cmd2/parsing.py | 15 +++++++---- cmd2/plugin.py | 6 ++++- cmd2/pt_utils.py | 20 ++++++++------ cmd2/py_bridge.py | 4 ++- cmd2/rich_utils.py | 11 ++++++-- cmd2/string_utils.py | 10 +++++-- cmd2/styles.py | 2 ++ cmd2/types.py | 2 ++ cmd2/utils.py | 20 +++++++------- 20 files changed, 152 insertions(+), 93 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 57a196e78..138d2eb7c 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 dataclasses import inspect @@ -22,33 +24,31 @@ cast, ) -from rich.text import Text - -from .constants import INFINITY -from .rich_utils import Cmd2GeneralConsole - -if TYPE_CHECKING: # pragma: no cover - from .cmd2 import Cmd - from rich.box import SIMPLE_HEAD from rich.table import ( Column, Table, ) +from rich.text import Text from .argparse_custom import ( ChoicesCallable, generate_range_error, ) -from .command_definition import CommandSet from .completion import ( CompletionItem, Completions, all_display_numeric, ) +from .constants import INFINITY from .exceptions import CompletionError +from .rich_utils import Cmd2GeneralConsole from .styles import Cmd2Style +if TYPE_CHECKING: # pragma: no cover + from .cmd2 import Cmd + from .command_definition import CommandSet + # If no table header is supplied, then this will be used instead DEFAULT_TABLE_HEADER: Sequence[str | Column] = ['Description'] @@ -166,7 +166,7 @@ class ArgparseCompleter: def __init__( self, parser: argparse.ArgumentParser, - cmd2_app: 'Cmd', + cmd2_app: Cmd, *, parent_tokens: Mapping[str, MutableSequence[str]] | None = None, ) -> None: diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2830a45a1..234823ea6 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -261,6 +261,8 @@ def get_choices(self) -> Choices: sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser` for more details. """ +from __future__ import annotations + import argparse import re import sys @@ -884,7 +886,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) -> type['ArgparseCompleter'] | None: # 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. @@ -900,7 +902,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> type 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. @@ -1186,7 +1188,7 @@ def __init__( suggest_on_error: bool = False, color: bool = False, *, - ap_completer_type: type['ArgparseCompleter'] | None = None, + ap_completer_type: type[ArgparseCompleter] | None = None, ) -> None: """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2. diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 4f78925cf..9d699fe06 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-untyped] diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8fff41a60..e69a27ff3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -22,10 +22,8 @@ Documentation: https://cmd2.readthedocs.io/ """ -# This module has many imports, quite a few of which are only -# infrequently utilized. To reduce the initial overhead of -# import this module, many of these imports are lazy-loaded -# i.e. we only import the module when we use it. +from __future__ import annotations + import argparse import contextlib import copy @@ -56,14 +54,13 @@ dataclass, field, ) -from types import FrameType from typing import ( IO, TYPE_CHECKING, Any, TextIO, + TypeAlias, TypeVar, - Union, cast, ) @@ -96,7 +93,6 @@ ) from . import rich_utils as ru from . import string_utils as su -from .argparse_custom import Cmd2ArgumentParser from .clipboard import ( get_paste_buffer, write_to_paste_buffer, @@ -147,13 +143,6 @@ RichPrintKwargs, ) from .styles import Cmd2Style -from .types import ( - ChoicesProviderUnbound, - CmdOrSet, - CompleterBound, - CompleterUnbound, - Matchable, -) with contextlib.suppress(ImportError): from IPython import start_ipython @@ -198,6 +187,24 @@ def __init__(self, msg: str = '') -> None: suggest_similar, ) +if TYPE_CHECKING: # pragma: no cover + StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] + ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] + from types import FrameType + + from .argparse_custom import Cmd2ArgumentParser + from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterBound, + CompleterUnbound, + Matchable, + ) + +else: + StaticArgParseBuilder = staticmethod + ClassArgParseBuilder = classmethod + class _SavedCmd2Env: """cmd2 environment settings that are backed up when entering an interactive Python shell.""" @@ -211,21 +218,13 @@ def __init__(self) -> None: DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024 -if TYPE_CHECKING: # pragma: no cover - StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] - ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] -else: - StaticArgParseBuilder = staticmethod - ClassArgParseBuilder = classmethod - - class _CommandParsers: """Create and store all command method argument parsers for a given Cmd instance. 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 @@ -1005,7 +1004,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 @@ -1098,7 +1097,7 @@ def find_subcommand( 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 @@ -2195,8 +2194,8 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar :param parser: the parser to examine :return: type of ArgparseCompleter """ - Completer = type[argparse_completer.ArgparseCompleter] | None # noqa: N806 - completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined] + CompleterType: TypeAlias = type[argparse_completer.ArgparseCompleter] | None + completer_type: CompleterType = parser.get_ap_completer_type() # type: ignore[attr-defined] if completer_type is None: completer_type = argparse_completer.DEFAULT_AP_COMPLETER @@ -5661,7 +5660,7 @@ def register_cmdfinalization_hook( def _resolve_func_self( self, cmd_support_func: Callable[..., Any], - cmd_self: Union[CommandSet, 'Cmd', None], + cmd_self: CommandSet | Cmd | None, ) -> object | None: """Attempt to resolve a candidate instance to pass as 'self'. diff --git a/cmd2/colors.py b/cmd2/colors.py index 1e6853c40..b4400b2ad 100644 --- a/cmd2/colors.py +++ b/cmd2/colors.py @@ -1,5 +1,7 @@ """Provides a convenient StrEnum for Rich color names.""" +from __future__ import annotations + import sys if sys.version_info >= (3, 11): diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index f98ab22f5..f3410a294 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, @@ -15,10 +17,10 @@ COMMAND_FUNC_PREFIX, ) from .exceptions import CommandSetRegistrationError -from .utils import Settable if TYPE_CHECKING: # pragma: no cover from .cmd2 import Cmd + from .utils import Settable #: Callable signature for a basic command function #: Further refinements are needed to define the input parameters @@ -98,7 +100,7 @@ def __init__(self) -> None: self._settable_prefix = self.__class__.__name__ @property - def _cmd(self) -> 'Cmd': + def _cmd(self) -> Cmd: """Property for child classes to access self.__cmd_internal. Using this property ensures that self.__cmd_internal has been set @@ -122,7 +124,7 @@ def _cmd(self) -> CustomCmdApp: raise CommandSetRegistrationError('This CommandSet is not registered') return self.__cmd_internal - def on_register(self, cmd: 'Cmd') -> None: + def on_register(self, cmd: 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/completion.py b/cmd2/completion.py index 91cd1f7f9..25b0c1dde 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -1,18 +1,15 @@ """Provides classes and functions related to command-line completion.""" +from __future__ import annotations + import re import sys -from collections.abc import ( - Collection, - Iterable, - Iterator, - Sequence, -) from dataclasses import ( dataclass, field, ) from typing import ( + TYPE_CHECKING, Any, cast, overload, @@ -28,7 +25,14 @@ from rich.protocol import is_renderable from . import rich_utils as ru -from . import utils + +if TYPE_CHECKING: + from collections.abc import ( + Collection, + Iterable, + Iterator, + Sequence, + ) # Regular expression to identify strings which we should sort numerically NUMERIC_RE = re.compile( @@ -143,6 +147,8 @@ class CompletionResultsBase: def __post_init__(self) -> None: """Finalize the object after initialization.""" + from . import utils + unique_items = utils.remove_duplicates(self.items) if not self.is_sorted: if all_display_numeric(unique_items): diff --git a/cmd2/constants.py b/cmd2/constants.py index 75c60662c..50bff4428 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -1,7 +1,10 @@ -"""Constants used throughout ``cmd2``.""" +"""Constants used throughout ``cmd2``. -# 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 +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 eb159d157..ff1b5c0db 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, @@ -65,7 +67,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['Cmd', Statement | str]: +def _parse_positionals(args: tuple[Any, ...]) -> tuple[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. diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index 5b25aefb1..8de727799 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 599bd13f2..4b5a976f3 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -1,13 +1,12 @@ """History management classes.""" +from __future__ import annotations + import json import re -from collections.abc import ( - Callable, - Iterable, -) from dataclasses import dataclass from typing import ( + TYPE_CHECKING, Any, overload, ) @@ -18,6 +17,12 @@ 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. @@ -120,7 +125,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()) @@ -223,7 +228,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) -> dict[int, 'HistoryItem']: + def span(self, span: str, include_persisted: bool = False) -> dict[int, HistoryItem]: """Return a slice of the History list. :param span: string containing an index or a slice @@ -272,7 +277,7 @@ def span(self, span: str, include_persisted: bool = False) -> dict[int, 'History return self._build_result_dictionary(start, end) - def str_search(self, search: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: + def str_search(self, search: str, include_persisted: bool = False) -> dict[int, HistoryItem]: """Find history items which contain a given string. :param search: the string to search for @@ -291,7 +296,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) -> dict[int, 'HistoryItem']: + def regex_search(self, regex: str, include_persisted: bool = False) -> dict[int, HistoryItem]: """Find history items which match a given regular expression. :param regex: the regular expression to search for. @@ -327,7 +332,7 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None - ) -> dict[int, 'HistoryItem']: + ) -> dict[int, HistoryItem]: """Build history search results. :param start: start index to search from @@ -348,7 +353,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 b0f059c54..d5e059c22 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -1,19 +1,17 @@ """Statement parsing classes for cmd2.""" +from __future__ import annotations + import re import shlex import sys -from collections.abc import ( - Iterable, - Mapping, - Sequence, -) from dataclasses import ( asdict, dataclass, field, ) from typing import ( + TYPE_CHECKING, Any, ClassVar, ) @@ -30,6 +28,13 @@ from . import string_utils as su from .exceptions import Cmd2ShlexError +if TYPE_CHECKING: + from collections.abc import ( + Iterable, + Mapping, + Sequence, + ) + def shlex_split(str_to_split: str) -> list[str]: """Split the string *str_to_split* using shell-like syntax. diff --git a/cmd2/plugin.py b/cmd2/plugin.py index 91b4af858..94b16d388 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,8 +1,12 @@ """Classes for the cmd2 lifecycle hooks that you can register multiple callback functions/methods with.""" +from __future__ import annotations + from dataclasses import dataclass +from typing import TYPE_CHECKING -from .parsing import Statement +if TYPE_CHECKING: + from .parsing import Statement @dataclass diff --git a/cmd2/pt_utils.py b/cmd2/pt_utils.py index c2a4ee6f3..64f06baf2 100644 --- a/cmd2/pt_utils.py +++ b/cmd2/pt_utils.py @@ -1,10 +1,8 @@ """Utilities for integrating prompt_toolkit with cmd2.""" +from __future__ import annotations + import re -from collections.abc import ( - Callable, - Iterable, -) from typing import ( TYPE_CHECKING, Any, @@ -16,7 +14,6 @@ Completer, Completion, ) -from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.history import History from prompt_toolkit.lexers import Lexer @@ -29,6 +26,13 @@ from . import string_utils as su if TYPE_CHECKING: # pragma: no cover + from collections.abc import ( + Callable, + Iterable, + ) + + from prompt_toolkit.document import Document + from .cmd2 import Cmd @@ -55,7 +59,7 @@ class Cmd2Completer(Completer): def __init__( self, - cmd_app: 'Cmd', + cmd_app: Cmd, custom_settings: utils.CustomCompletionSettings | None = None, ) -> None: """Initialize prompt_toolkit based completer class.""" @@ -155,7 +159,7 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab class Cmd2History(History): """History that bridges cmd2's history storage with prompt_toolkit.""" - def __init__(self, cmd_app: 'Cmd') -> None: + def __init__(self, cmd_app: Cmd) -> None: """Initialize prompt_toolkit based history wrapper class.""" super().__init__() self.cmd_app = cmd_app @@ -195,7 +199,7 @@ class Cmd2Lexer(Lexer): def __init__( self, - cmd_app: 'Cmd', + cmd_app: Cmd, command_color: str = 'ansigreen', alias_color: str = 'ansicyan', macro_color: str = 'ansimagenta', diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 224aa06da..22c259423 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 from typing import ( @@ -79,7 +81,7 @@ class PyBridge: Defaults to True. """ - def __init__(self, cmd2_app: 'Cmd', *, add_to_history: bool = True) -> None: + def __init__(self, cmd2_app: Cmd, *, add_to_history: bool = True) -> None: """Initialize PyBridge instances.""" self._cmd2_app = cmd2_app self._add_to_history = add_to_history diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index dcd5d15b7..031afe4c1 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -1,10 +1,12 @@ """Provides common utilities to support Rich in cmd2-based applications.""" +from __future__ import annotations + import re -from collections.abc import Mapping from enum import Enum from typing import ( IO, + TYPE_CHECKING, Any, TypedDict, ) @@ -19,7 +21,6 @@ from rich.padding import Padding from rich.pretty import is_expandable from rich.protocol import rich_cast -from rich.style import StyleType from rich.table import ( Column, Table, @@ -30,6 +31,12 @@ from .styles import DEFAULT_CMD2_STYLES +if TYPE_CHECKING: + from collections.abc import Mapping + + from rich.style import StyleType + + # Matches ANSI SGR (Select Graphic Rendition) sequences for text styling. # \x1b[ - the CSI (Control Sequence Introducer) # [0-9;]* - zero or more digits or semicolons (parameters for the style) diff --git a/cmd2/string_utils.py b/cmd2/string_utils.py index fc4e19556..5342c752b 100644 --- a/cmd2/string_utils.py +++ b/cmd2/string_utils.py @@ -5,12 +5,18 @@ full-width characters (like those used in CJK languages). """ -from rich.align import AlignMethod -from rich.style import StyleType +from __future__ import annotations + +from typing import TYPE_CHECKING + from rich.text import Text from . import rich_utils as ru +if TYPE_CHECKING: + from rich.align import AlignMethod + from rich.style import StyleType + def align( val: str, diff --git a/cmd2/styles.py b/cmd2/styles.py index 4fb86d72b..4a0abeb62 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -24,6 +24,8 @@ """ +from __future__ import annotations + import sys from rich.style import ( diff --git a/cmd2/types.py b/cmd2/types.py index 2dfa6af95..ab6fa8cc2 100644 --- a/cmd2/types.py +++ b/cmd2/types.py @@ -1,5 +1,7 @@ """Defines common types used throughout cmd2.""" +from __future__ import annotations + from collections.abc import ( Callable, Mapping, diff --git a/cmd2/utils.py b/cmd2/utils.py index 32fe17342..0b32bbc44 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1,6 +1,7 @@ """Shared utility functions.""" -import argparse +from __future__ import annotations + import contextlib import functools import glob @@ -22,21 +23,21 @@ Any, TextIO, TypeVar, - Union, cast, ) from . import constants from . import string_utils as su -from .completion import Choices -from .types import ( - ChoicesProviderUnbound, - CmdOrSet, - CompleterUnbound, -) if TYPE_CHECKING: # pragma: no cover PopenTextIO = subprocess.Popen[str] + import argparse + + from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterUnbound, + ) else: PopenTextIO = subprocess.Popen @@ -114,6 +115,7 @@ def __init__( :param completer: completion function that provides choices for this argument """ if val_type is bool: + from .completion import Choices def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: """Tab complete lowercase boolean values.""" @@ -397,7 +399,7 @@ class StdSim: def __init__( self, - inner_stream: Union[TextIO, 'StdSim'], + inner_stream: TextIO | StdSim, *, echo: bool = False, encoding: str = 'utf-8', From 6e444f4a7608700f37f6ccd02e56bdf07ad4e64e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 20:39:20 -0500 Subject: [PATCH 5/7] Revert "Added "from __future__ import annotations" to each cmd2 module." This reverts commit bcf046654275f5818c97e14754c4d49f395cc131. --- cmd2/argparse_completer.py | 20 +++++++------- cmd2/argparse_custom.py | 8 +++--- cmd2/clipboard.py | 2 -- cmd2/cmd2.py | 55 +++++++++++++++++++------------------- cmd2/colors.py | 2 -- cmd2/command_definition.py | 8 +++--- cmd2/completion.py | 20 +++++--------- cmd2/constants.py | 9 +++---- cmd2/decorators.py | 4 +-- cmd2/exceptions.py | 2 -- cmd2/history.py | 25 +++++++---------- cmd2/parsing.py | 15 ++++------- cmd2/plugin.py | 6 +---- cmd2/pt_utils.py | 20 ++++++-------- cmd2/py_bridge.py | 4 +-- cmd2/rich_utils.py | 11 ++------ cmd2/string_utils.py | 10 ++----- cmd2/styles.py | 2 -- cmd2/types.py | 2 -- cmd2/utils.py | 20 +++++++------- 20 files changed, 93 insertions(+), 152 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 138d2eb7c..57a196e78 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -3,8 +3,6 @@ See the header of argparse_custom.py for instructions on how to use these features. """ -from __future__ import annotations - import argparse import dataclasses import inspect @@ -24,31 +22,33 @@ cast, ) +from rich.text import Text + +from .constants import INFINITY +from .rich_utils import Cmd2GeneralConsole + +if TYPE_CHECKING: # pragma: no cover + from .cmd2 import Cmd + from rich.box import SIMPLE_HEAD from rich.table import ( Column, Table, ) -from rich.text import Text from .argparse_custom import ( ChoicesCallable, generate_range_error, ) +from .command_definition import CommandSet from .completion import ( CompletionItem, Completions, all_display_numeric, ) -from .constants import INFINITY from .exceptions import CompletionError -from .rich_utils import Cmd2GeneralConsole from .styles import Cmd2Style -if TYPE_CHECKING: # pragma: no cover - from .cmd2 import Cmd - from .command_definition import CommandSet - # If no table header is supplied, then this will be used instead DEFAULT_TABLE_HEADER: Sequence[str | Column] = ['Description'] @@ -166,7 +166,7 @@ class ArgparseCompleter: def __init__( self, parser: argparse.ArgumentParser, - cmd2_app: Cmd, + cmd2_app: 'Cmd', *, parent_tokens: Mapping[str, MutableSequence[str]] | None = None, ) -> None: diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 234823ea6..2830a45a1 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -261,8 +261,6 @@ def get_choices(self) -> Choices: sub-parser from a sub-parsers group. See _SubParsersAction_remove_parser` for more details. """ -from __future__ import annotations - import argparse import re import sys @@ -886,7 +884,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) -> type[ArgparseCompleter] | None: # 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. @@ -902,7 +900,7 @@ def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> type 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. @@ -1188,7 +1186,7 @@ def __init__( suggest_on_error: bool = False, color: bool = False, *, - ap_completer_type: type[ArgparseCompleter] | None = None, + ap_completer_type: type['ArgparseCompleter'] | None = None, ) -> None: """Initialize the Cmd2ArgumentParser instance, a custom ArgumentParser added by cmd2. diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py index 9d699fe06..4f78925cf 100644 --- a/cmd2/clipboard.py +++ b/cmd2/clipboard.py @@ -1,7 +1,5 @@ """Module provides basic ability to copy from and paste to the clipboard/pastebuffer.""" -from __future__ import annotations - import typing import pyperclip # type: ignore[import-untyped] diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e69a27ff3..8fff41a60 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -22,8 +22,10 @@ Documentation: https://cmd2.readthedocs.io/ """ -from __future__ import annotations - +# This module has many imports, quite a few of which are only +# infrequently utilized. To reduce the initial overhead of +# import this module, many of these imports are lazy-loaded +# i.e. we only import the module when we use it. import argparse import contextlib import copy @@ -54,13 +56,14 @@ dataclass, field, ) +from types import FrameType from typing import ( IO, TYPE_CHECKING, Any, TextIO, - TypeAlias, TypeVar, + Union, cast, ) @@ -93,6 +96,7 @@ ) from . import rich_utils as ru from . import string_utils as su +from .argparse_custom import Cmd2ArgumentParser from .clipboard import ( get_paste_buffer, write_to_paste_buffer, @@ -143,6 +147,13 @@ RichPrintKwargs, ) from .styles import Cmd2Style +from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterBound, + CompleterUnbound, + Matchable, +) with contextlib.suppress(ImportError): from IPython import start_ipython @@ -187,24 +198,6 @@ def __init__(self, msg: str = '') -> None: suggest_similar, ) -if TYPE_CHECKING: # pragma: no cover - StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] - ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] - from types import FrameType - - from .argparse_custom import Cmd2ArgumentParser - from .types import ( - ChoicesProviderUnbound, - CmdOrSet, - CompleterBound, - CompleterUnbound, - Matchable, - ) - -else: - StaticArgParseBuilder = staticmethod - ClassArgParseBuilder = classmethod - class _SavedCmd2Env: """cmd2 environment settings that are backed up when entering an interactive Python shell.""" @@ -218,13 +211,21 @@ def __init__(self) -> None: DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024 +if TYPE_CHECKING: # pragma: no cover + StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] + ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] +else: + StaticArgParseBuilder = staticmethod + ClassArgParseBuilder = classmethod + + class _CommandParsers: """Create and store all command method argument parsers for a given Cmd instance. 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 @@ -1004,7 +1005,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: CommandSet | Cmd) -> None: + def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: """Register subcommands with their base command. :param cmdset: CommandSet or cmd2.Cmd subclass containing subcommands @@ -1097,7 +1098,7 @@ def find_subcommand( break - def _unregister_subcommands(self, cmdset: CommandSet | Cmd) -> None: + def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: """Unregister subcommands from their base command. :param cmdset: CommandSet containing subcommands @@ -2194,8 +2195,8 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar :param parser: the parser to examine :return: type of ArgparseCompleter """ - CompleterType: TypeAlias = type[argparse_completer.ArgparseCompleter] | None - completer_type: CompleterType = parser.get_ap_completer_type() # type: ignore[attr-defined] + Completer = type[argparse_completer.ArgparseCompleter] | None # noqa: N806 + completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined] if completer_type is None: completer_type = argparse_completer.DEFAULT_AP_COMPLETER @@ -5660,7 +5661,7 @@ def register_cmdfinalization_hook( def _resolve_func_self( self, cmd_support_func: Callable[..., Any], - cmd_self: CommandSet | Cmd | None, + cmd_self: Union[CommandSet, 'Cmd', None], ) -> object | None: """Attempt to resolve a candidate instance to pass as 'self'. diff --git a/cmd2/colors.py b/cmd2/colors.py index b4400b2ad..1e6853c40 100644 --- a/cmd2/colors.py +++ b/cmd2/colors.py @@ -1,7 +1,5 @@ """Provides a convenient StrEnum for Rich color names.""" -from __future__ import annotations - import sys if sys.version_info >= (3, 11): diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py index f3410a294..f98ab22f5 100644 --- a/cmd2/command_definition.py +++ b/cmd2/command_definition.py @@ -1,7 +1,5 @@ """Supports the definition of commands in separate classes to be composed into cmd2.Cmd.""" -from __future__ import annotations - from collections.abc import ( Callable, Mapping, @@ -17,10 +15,10 @@ COMMAND_FUNC_PREFIX, ) from .exceptions import CommandSetRegistrationError +from .utils import Settable if TYPE_CHECKING: # pragma: no cover from .cmd2 import Cmd - from .utils import Settable #: Callable signature for a basic command function #: Further refinements are needed to define the input parameters @@ -100,7 +98,7 @@ def __init__(self) -> None: self._settable_prefix = self.__class__.__name__ @property - def _cmd(self) -> Cmd: + def _cmd(self) -> 'Cmd': """Property for child classes to access self.__cmd_internal. Using this property ensures that self.__cmd_internal has been set @@ -124,7 +122,7 @@ def _cmd(self) -> CustomCmdApp: raise CommandSetRegistrationError('This CommandSet is not registered') return self.__cmd_internal - def on_register(self, cmd: Cmd) -> None: + def on_register(self, cmd: '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/completion.py b/cmd2/completion.py index 25b0c1dde..91cd1f7f9 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -1,15 +1,18 @@ """Provides classes and functions related to command-line completion.""" -from __future__ import annotations - import re import sys +from collections.abc import ( + Collection, + Iterable, + Iterator, + Sequence, +) from dataclasses import ( dataclass, field, ) from typing import ( - TYPE_CHECKING, Any, cast, overload, @@ -25,14 +28,7 @@ from rich.protocol import is_renderable from . import rich_utils as ru - -if TYPE_CHECKING: - from collections.abc import ( - Collection, - Iterable, - Iterator, - Sequence, - ) +from . import utils # Regular expression to identify strings which we should sort numerically NUMERIC_RE = re.compile( @@ -147,8 +143,6 @@ class CompletionResultsBase: def __post_init__(self) -> None: """Finalize the object after initialization.""" - from . import utils - unique_items = utils.remove_duplicates(self.items) if not self.is_sorted: if all_display_numeric(unique_items): diff --git a/cmd2/constants.py b/cmd2/constants.py index 50bff4428..75c60662c 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -1,10 +1,7 @@ -"""Constants used throughout ``cmd2``. +"""Constants used throughout ``cmd2``.""" -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 +# 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 INFINITY = float('inf') diff --git a/cmd2/decorators.py b/cmd2/decorators.py index ff1b5c0db..eb159d157 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -1,7 +1,5 @@ """Decorators for ``cmd2`` commands.""" -from __future__ import annotations - import argparse from collections.abc import ( Callable, @@ -67,7 +65,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[Cmd, Statement | str]: +def _parse_positionals(args: tuple[Any, ...]) -> tuple['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. diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py index 8de727799..5b25aefb1 100644 --- a/cmd2/exceptions.py +++ b/cmd2/exceptions.py @@ -1,7 +1,5 @@ """Custom exceptions for cmd2.""" -from __future__ import annotations - from typing import Any ############################################################################################################ diff --git a/cmd2/history.py b/cmd2/history.py index 4b5a976f3..599bd13f2 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -1,12 +1,13 @@ """History management classes.""" -from __future__ import annotations - import json import re +from collections.abc import ( + Callable, + Iterable, +) from dataclasses import dataclass from typing import ( - TYPE_CHECKING, Any, overload, ) @@ -17,12 +18,6 @@ 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. @@ -125,7 +120,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()) @@ -228,7 +223,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) -> dict[int, HistoryItem]: + def span(self, span: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Return a slice of the History list. :param span: string containing an index or a slice @@ -277,7 +272,7 @@ def span(self, span: str, include_persisted: bool = False) -> dict[int, HistoryI return self._build_result_dictionary(start, end) - def str_search(self, search: str, include_persisted: bool = False) -> dict[int, HistoryItem]: + def str_search(self, search: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Find history items which contain a given string. :param search: the string to search for @@ -296,7 +291,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) -> dict[int, HistoryItem]: + def regex_search(self, regex: str, include_persisted: bool = False) -> dict[int, 'HistoryItem']: """Find history items which match a given regular expression. :param regex: the regular expression to search for. @@ -332,7 +327,7 @@ def truncate(self, max_length: int) -> None: def _build_result_dictionary( self, start: int, end: int, filter_func: Callable[[HistoryItem], bool] | None = None - ) -> dict[int, HistoryItem]: + ) -> dict[int, 'HistoryItem']: """Build history search results. :param start: start index to search from @@ -353,7 +348,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 d5e059c22..b0f059c54 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -1,17 +1,19 @@ """Statement parsing classes for cmd2.""" -from __future__ import annotations - import re import shlex import sys +from collections.abc import ( + Iterable, + Mapping, + Sequence, +) from dataclasses import ( asdict, dataclass, field, ) from typing import ( - TYPE_CHECKING, Any, ClassVar, ) @@ -28,13 +30,6 @@ from . import string_utils as su from .exceptions import Cmd2ShlexError -if TYPE_CHECKING: - from collections.abc import ( - Iterable, - Mapping, - Sequence, - ) - def shlex_split(str_to_split: str) -> list[str]: """Split the string *str_to_split* using shell-like syntax. diff --git a/cmd2/plugin.py b/cmd2/plugin.py index 94b16d388..91b4af858 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,12 +1,8 @@ """Classes for the cmd2 lifecycle hooks that you can register multiple callback functions/methods with.""" -from __future__ import annotations - from dataclasses import dataclass -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from .parsing import Statement +from .parsing import Statement @dataclass diff --git a/cmd2/pt_utils.py b/cmd2/pt_utils.py index 64f06baf2..c2a4ee6f3 100644 --- a/cmd2/pt_utils.py +++ b/cmd2/pt_utils.py @@ -1,8 +1,10 @@ """Utilities for integrating prompt_toolkit with cmd2.""" -from __future__ import annotations - import re +from collections.abc import ( + Callable, + Iterable, +) from typing import ( TYPE_CHECKING, Any, @@ -14,6 +16,7 @@ Completer, Completion, ) +from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.history import History from prompt_toolkit.lexers import Lexer @@ -26,13 +29,6 @@ from . import string_utils as su if TYPE_CHECKING: # pragma: no cover - from collections.abc import ( - Callable, - Iterable, - ) - - from prompt_toolkit.document import Document - from .cmd2 import Cmd @@ -59,7 +55,7 @@ class Cmd2Completer(Completer): def __init__( self, - cmd_app: Cmd, + cmd_app: 'Cmd', custom_settings: utils.CustomCompletionSettings | None = None, ) -> None: """Initialize prompt_toolkit based completer class.""" @@ -159,7 +155,7 @@ def get_completions(self, document: Document, _complete_event: object) -> Iterab class Cmd2History(History): """History that bridges cmd2's history storage with prompt_toolkit.""" - def __init__(self, cmd_app: Cmd) -> None: + def __init__(self, cmd_app: 'Cmd') -> None: """Initialize prompt_toolkit based history wrapper class.""" super().__init__() self.cmd_app = cmd_app @@ -199,7 +195,7 @@ class Cmd2Lexer(Lexer): def __init__( self, - cmd_app: Cmd, + cmd_app: 'Cmd', command_color: str = 'ansigreen', alias_color: str = 'ansicyan', macro_color: str = 'ansimagenta', diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py index 22c259423..224aa06da 100644 --- a/cmd2/py_bridge.py +++ b/cmd2/py_bridge.py @@ -3,8 +3,6 @@ Maintains a reasonable degree of isolation between the two. """ -from __future__ import annotations - import sys from contextlib import redirect_stderr from typing import ( @@ -81,7 +79,7 @@ class PyBridge: Defaults to True. """ - def __init__(self, cmd2_app: Cmd, *, add_to_history: bool = True) -> None: + def __init__(self, cmd2_app: 'Cmd', *, add_to_history: bool = True) -> None: """Initialize PyBridge instances.""" self._cmd2_app = cmd2_app self._add_to_history = add_to_history diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 031afe4c1..dcd5d15b7 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -1,12 +1,10 @@ """Provides common utilities to support Rich in cmd2-based applications.""" -from __future__ import annotations - import re +from collections.abc import Mapping from enum import Enum from typing import ( IO, - TYPE_CHECKING, Any, TypedDict, ) @@ -21,6 +19,7 @@ from rich.padding import Padding from rich.pretty import is_expandable from rich.protocol import rich_cast +from rich.style import StyleType from rich.table import ( Column, Table, @@ -31,12 +30,6 @@ from .styles import DEFAULT_CMD2_STYLES -if TYPE_CHECKING: - from collections.abc import Mapping - - from rich.style import StyleType - - # Matches ANSI SGR (Select Graphic Rendition) sequences for text styling. # \x1b[ - the CSI (Control Sequence Introducer) # [0-9;]* - zero or more digits or semicolons (parameters for the style) diff --git a/cmd2/string_utils.py b/cmd2/string_utils.py index 5342c752b..fc4e19556 100644 --- a/cmd2/string_utils.py +++ b/cmd2/string_utils.py @@ -5,18 +5,12 @@ full-width characters (like those used in CJK languages). """ -from __future__ import annotations - -from typing import TYPE_CHECKING - +from rich.align import AlignMethod +from rich.style import StyleType from rich.text import Text from . import rich_utils as ru -if TYPE_CHECKING: - from rich.align import AlignMethod - from rich.style import StyleType - def align( val: str, diff --git a/cmd2/styles.py b/cmd2/styles.py index 4a0abeb62..4fb86d72b 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -24,8 +24,6 @@ """ -from __future__ import annotations - import sys from rich.style import ( diff --git a/cmd2/types.py b/cmd2/types.py index ab6fa8cc2..2dfa6af95 100644 --- a/cmd2/types.py +++ b/cmd2/types.py @@ -1,7 +1,5 @@ """Defines common types used throughout cmd2.""" -from __future__ import annotations - from collections.abc import ( Callable, Mapping, diff --git a/cmd2/utils.py b/cmd2/utils.py index 0b32bbc44..32fe17342 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -1,7 +1,6 @@ """Shared utility functions.""" -from __future__ import annotations - +import argparse import contextlib import functools import glob @@ -23,21 +22,21 @@ Any, TextIO, TypeVar, + Union, cast, ) from . import constants from . import string_utils as su +from .completion import Choices +from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterUnbound, +) if TYPE_CHECKING: # pragma: no cover PopenTextIO = subprocess.Popen[str] - import argparse - - from .types import ( - ChoicesProviderUnbound, - CmdOrSet, - CompleterUnbound, - ) else: PopenTextIO = subprocess.Popen @@ -115,7 +114,6 @@ def __init__( :param completer: completion function that provides choices for this argument """ if val_type is bool: - from .completion import Choices def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: """Tab complete lowercase boolean values.""" @@ -399,7 +397,7 @@ class StdSim: def __init__( self, - inner_stream: TextIO | StdSim, + inner_stream: Union[TextIO, 'StdSim'], *, echo: bool = False, encoding: str = 'utf-8', From 7f38e850358f1b180c52be1281040153341afc88 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 20:46:39 -0500 Subject: [PATCH 6/7] Moved some imports. Fixed circular import between utils.py and completion.py. --- cmd2/cmd2.py | 20 ++++++++++---------- cmd2/completion.py | 3 ++- cmd2/utils.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8fff41a60..eee4aa5d3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -62,6 +62,7 @@ TYPE_CHECKING, Any, TextIO, + TypeAlias, TypeVar, Union, cast, @@ -198,6 +199,13 @@ def __init__(self, msg: str = '') -> None: suggest_similar, ) +if TYPE_CHECKING: # pragma: no cover + StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] + ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] +else: + StaticArgParseBuilder = staticmethod + ClassArgParseBuilder = classmethod + class _SavedCmd2Env: """cmd2 environment settings that are backed up when entering an interactive Python shell.""" @@ -211,14 +219,6 @@ def __init__(self) -> None: DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function', 'completer_function']) # noqa: PYI024 -if TYPE_CHECKING: # pragma: no cover - StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser] - ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser] -else: - StaticArgParseBuilder = staticmethod - ClassArgParseBuilder = classmethod - - class _CommandParsers: """Create and store all command method argument parsers for a given Cmd instance. @@ -2195,8 +2195,8 @@ def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argpar :param parser: the parser to examine :return: type of ArgparseCompleter """ - Completer = type[argparse_completer.ArgparseCompleter] | None # noqa: N806 - completer_type: Completer = parser.get_ap_completer_type() # type: ignore[attr-defined] + APCompleterType: TypeAlias = type[argparse_completer.ArgparseCompleter] | None + completer_type: APCompleterType = parser.get_ap_completer_type() # type: ignore[attr-defined] if completer_type is None: completer_type = argparse_completer.DEFAULT_AP_COMPLETER diff --git a/cmd2/completion.py b/cmd2/completion.py index 91cd1f7f9..ac5476a2a 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -28,7 +28,6 @@ from rich.protocol import is_renderable from . import rich_utils as ru -from . import utils # Regular expression to identify strings which we should sort numerically NUMERIC_RE = re.compile( @@ -143,6 +142,8 @@ class CompletionResultsBase: def __post_init__(self) -> None: """Finalize the object after initialization.""" + from . import utils + unique_items = utils.remove_duplicates(self.items) if not self.is_sorted: if all_display_numeric(unique_items): diff --git a/cmd2/utils.py b/cmd2/utils.py index 32fe17342..32459ae83 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -28,7 +28,6 @@ from . import constants from . import string_utils as su -from .completion import Choices from .types import ( ChoicesProviderUnbound, CmdOrSet, @@ -114,6 +113,7 @@ def __init__( :param completer: completion function that provides choices for this argument """ if val_type is bool: + from .completion import Choices def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: """Tab complete lowercase boolean values.""" From c8ffd8b38b627c71948352c145cb9e50422e071b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 25 Feb 2026 20:54:19 -0500 Subject: [PATCH 7/7] Removed Matchable type since it's only used one place. --- cmd2/cmd2.py | 3 +-- cmd2/types.py | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index eee4aa5d3..0a604cb05 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -153,7 +153,6 @@ CmdOrSet, CompleterBound, CompleterUnbound, - Matchable, ) with contextlib.suppress(ImportError): @@ -1823,7 +1822,7 @@ def basic_complete( line: str, # noqa: ARG002 begidx: int, # noqa: ARG002 endidx: int, # noqa: ARG002 - match_against: Iterable[Matchable], + match_against: Iterable[str | CompletionItem], *, sort: bool = True, ) -> Completions: diff --git a/cmd2/types.py b/cmd2/types.py index 2dfa6af95..c1c2fada8 100644 --- a/cmd2/types.py +++ b/cmd2/types.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: # pragma: no cover from .cmd2 import Cmd from .command_definition import CommandSet - from .completion import Choices, CompletionItem, Completions + from .completion import Choices, Completions # A Cmd or CommandSet CmdOrSet = TypeVar("CmdOrSet", bound=Union["Cmd", "CommandSet"]) @@ -27,11 +27,6 @@ # Represents the parsed tokens from argparse during completion ArgTokens: TypeAlias = Mapping[str, Sequence[str]] -# Represents a type that can be matched against when completing. -# Strings are matched directly while CompletionItems are matched -# against their 'text' member. -Matchable: TypeAlias = Union[str, "CompletionItem"] - ################################################## # choices_provider function types ##################################################