Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 14 additions & 13 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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)


############################################################################################################
Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
45 changes: 23 additions & 22 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
TYPE_CHECKING,
Any,
TextIO,
TypeAlias,
TypeVar,
Union,
cast,
Expand Down Expand Up @@ -107,12 +108,8 @@
)
from .completion import (
Choices,
ChoicesProviderUnbound,
CompleterBound,
CompleterUnbound,
CompletionItem,
Completions,
Matchable,
)
from .constants import (
CLASS_ATTR_DEFAULT_HELP_CATEGORY,
Expand All @@ -121,7 +118,6 @@
HELP_FUNC_PREFIX,
)
from .decorators import (
CommandParent,
as_subcommand_to,
with_argparser,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions cmd2/command_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
57 changes: 2 additions & 55 deletions cmd2/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,23 @@
import re
import sys
from collections.abc import (
Callable,
Collection,
Iterable,
Iterator,
Mapping,
Sequence,
)
from dataclasses import (
dataclass,
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:
Expand All @@ -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(
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Loading
Loading