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 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..0a604cb05 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -62,6 +62,7 @@ TYPE_CHECKING, Any, TextIO, + TypeAlias, TypeVar, Union, cast, @@ -107,12 +108,8 @@ ) from .completion import ( Choices, - ChoicesProviderUnbound, - CompleterBound, - CompleterUnbound, CompletionItem, Completions, - Matchable, ) from .constants import ( CLASS_ATTR_DEFAULT_HELP_CATEGORY, @@ -121,7 +118,6 @@ HELP_FUNC_PREFIX, ) from .decorators import ( - CommandParent, as_subcommand_to, with_argparser, ) @@ -152,6 +148,12 @@ RichPrintKwargs, ) from .styles import Cmd2Style +from .types import ( + ChoicesProviderUnbound, + CmdOrSet, + CompleterBound, + CompleterUnbound, +) with contextlib.suppress(ImportError): from IPython import start_ipython @@ -196,6 +198,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.""" @@ -209,14 +218,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. @@ -840,7 +841,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 +850,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 @@ -1821,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: @@ -2193,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] + 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 @@ -3283,8 +3284,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 +3316,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..ac5476a2a 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: @@ -36,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( @@ -151,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): @@ -264,49 +257,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..c1c2fada8 --- /dev/null +++ b/cmd2/types.py @@ -0,0 +1,61 @@ +"""Defines common types used throughout cmd2.""" + +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, 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]] + +################################################## +# 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..32459ae83 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -28,15 +28,13 @@ 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 - from .decorators import CommandParent - PopenTextIO = subprocess.Popen[str] else: PopenTextIO = subprocess.Popen @@ -78,8 +76,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. @@ -115,8 +113,9 @@ 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: "CommandParent") -> Choices: + def get_bool_choices(_cmd2_self: CmdOrSet) -> Choices: """Tab complete lowercase boolean values.""" return Choices.from_values(['true', 'false'])