From 60a6ec1ec8f62a5acd24052e02b88be77980a62d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 21 Feb 2026 16:19:15 -0500 Subject: [PATCH 1/3] Use more generic types where possible for input parameters Use generic types like the following where possible: - Iterable - Mapping - MutableSequence - Sequence --- cmd2/argparse_completer.py | 18 ++++++++----- cmd2/cmd2.py | 52 ++++++++++++++++++++------------------ cmd2/decorators.py | 2 +- cmd2/parsing.py | 7 ++--- cmd2/utils.py | 11 ++++---- 5 files changed, 50 insertions(+), 40 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index c2643b60..57a196e7 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -10,7 +10,11 @@ defaultdict, deque, ) -from collections.abc import Sequence +from collections.abc import ( + Mapping, + MutableSequence, + Sequence, +) from typing import ( IO, TYPE_CHECKING, @@ -164,13 +168,13 @@ def __init__( parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, - parent_tokens: dict[str, list[str]] | None = None, + parent_tokens: Mapping[str, MutableSequence[str]] | None = None, ) -> None: """Create an ArgparseCompleter. :param parser: ArgumentParser instance :param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter - :param parent_tokens: optional dictionary mapping parent parsers' arg names to their tokens + :param parent_tokens: optional Mapping of parent parsers' arg names to their tokens This is only used by ArgparseCompleter when recursing on subcommand parsers Defaults to None """ @@ -216,7 +220,7 @@ def complete( line: str, begidx: int, endidx: int, - tokens: list[str], + tokens: Sequence[str], *, cmd_set: CommandSet | None = None, ) -> Completions: @@ -226,7 +230,7 @@ def complete( :param line: the current input line with leading whitespace removed :param begidx: the beginning index of the prefix text :param endidx: the ending index of the prefix text - :param tokens: list of argument tokens being passed to the parser + :param tokens: Sequence of argument tokens being passed to the parser :param cmd_set: if completing a command, the CommandSet the command's function belongs to, if applicable. Defaults to None. :return: a Completions object @@ -638,7 +642,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Completion completion_table=capture.get(), ) - def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: list[str]) -> Completions: + def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: Sequence[str]) -> Completions: """Supports cmd2's help command in the completion of subcommand names. :param text: the string prefix we are attempting to match (all matches must begin with it) @@ -664,7 +668,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in break return Completions() - def print_help(self, tokens: list[str], file: IO[str] | None = None) -> None: + def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None: """Supports cmd2's help command in the printing of help text. :param tokens: arguments passed to help command diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c76172f0..10b8bebd 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -45,6 +45,8 @@ Callable, Iterable, Mapping, + MutableSequence, + Sequence, ) from types import FrameType from typing import ( @@ -299,14 +301,14 @@ def __init__( include_ipy: bool = False, include_py: bool = False, intro: RenderableType = '', - multiline_commands: list[str] | None = None, + multiline_commands: Iterable[str] | None = None, persistent_history_file: str = '', persistent_history_length: int = 1000, - shortcuts: dict[str, str] | None = None, + shortcuts: Mapping[str, str] | None = None, silence_startup_script: bool = False, startup_script: str = '', suggest_similar_command: bool = False, - terminators: list[str] | None = None, + terminators: Iterable[str] | None = None, ) -> None: """Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package. @@ -337,24 +339,24 @@ def __init__( :param include_ipy: should the "ipy" command be included for an embedded IPython shell :param include_py: should the "py" command be included for an embedded Python shell :param intro: introduction to display at startup - :param multiline_commands: list of commands allowed to accept multi-line input + :param multiline_commands: Iterable of commands allowed to accept multi-line input :param persistent_history_file: file path to load a persistent cmd2 command history from :param persistent_history_length: max number of history items to write to the persistent history file - :param shortcuts: dictionary containing shortcuts for commands. If not supplied, + :param shortcuts: Mapping containing shortcuts for commands. If not supplied, then defaults to constants.DEFAULT_SHORTCUTS. If you do not want - any shortcuts, pass an empty dictionary. + any shortcuts, pass None and an empty dictionary will be created. :param silence_startup_script: if ``True``, then the startup script's output will be suppressed. Anything written to stderr will still display. :param startup_script: file path to a script to execute at startup :param suggest_similar_command: if ``True``, then when a command is not found, [cmd2.Cmd][] will look for similar commands and suggest them. - :param terminators: list of characters that terminate a command. These are mainly + :param terminators: Iterable of characters that terminate a command. These are mainly intended for terminating multiline commands, but will also terminate single-line commands. If not supplied, the default is a semicolon. If your app only contains single-line commands and you want terminators to be treated as literals by the parser, - then set this to an empty list. + then set this to None. """ # Check if py or ipy need to be disabled in this instance if not include_py: @@ -996,7 +998,9 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None: f"Could not find argparser for command '{command_name}' needed by subcommand: {method}" ) - def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser: + def find_subcommand( + action: argparse.ArgumentParser, subcmd_names: MutableSequence[str] + ) -> argparse.ArgumentParser: if not subcmd_names: return action cur_subcmd = subcmd_names.pop(0) @@ -2766,7 +2770,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> def runcmds_plus_hooks( self, - cmds: list[HistoryItem] | list[str], + cmds: Iterable[HistoryItem] | Iterable[str], *, add_to_history: bool = True, stop_on_keyboard_interrupt: bool = False, @@ -3169,7 +3173,7 @@ def default(self, statement: Statement) -> bool | None: self.perror(err_msg, style=None) return None - def completedefault(self, *_ignored: list[str]) -> Completions: + def completedefault(self, *_ignored: Sequence[str]) -> Completions: """Call to complete an input line when no command-specific complete_*() method is available. This method is only called for non-argparse-based commands. @@ -3185,7 +3189,7 @@ def read_input( self, prompt: str = '', *, - history: list[str] | None = None, + history: Iterable[str] | None = None, completion_mode: utils.CompletionMode = utils.CompletionMode.NONE, preserve_quotes: bool = False, choices: Iterable[Any] | None = None, @@ -3198,7 +3202,7 @@ def read_input( Also supports completion and up-arrow history while input is being entered. :param prompt: prompt to display to user - :param history: optional list of strings to use for up-arrow history. If completion_mode is + :param history: optional Iterable of strings to use for up-arrow history. If completion_mode is CompletionMode.COMMANDS and this is None, then cmd2's command list history will be used. The passed in history will not be edited. It is the caller's responsibility to add the returned input to history if desired. Defaults to None. @@ -3873,7 +3877,7 @@ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) return self.basic_complete(text, line, begidx, endidx, strs_to_match) def complete_help_subcommands( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Mapping[str, Sequence[str]] ) -> Completions: """Completes the subcommands argument of help.""" # Make sure we have a command whose subcommands we will complete @@ -4014,13 +4018,13 @@ def do_help(self, args: argparse.Namespace) -> None: self.perror(err_msg, style=None) self.last_result = False - def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: int) -> None: # noqa: ARG002 + def print_topics(self, header: str, cmds: Sequence[str] | None, cmdlen: int, maxcol: int) -> None: # noqa: ARG002 """Print groups of commands and topics in columns and an optional header. Override of cmd's print_topics() to use Rich. :param header: string to print above commands being printed - :param cmds: list of topics to print + :param cmds: Sequence of topics to print :param cmdlen: unused, even by cmd's version :param maxcol: max number of display columns to fit into """ @@ -4039,7 +4043,7 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: self.columnize(cmds, maxcol) self.poutput() - def _print_documented_command_topics(self, header: str, cmds: list[str], verbose: bool) -> None: + def _print_documented_command_topics(self, header: str, cmds: Sequence[str], verbose: bool) -> None: """Print topics which are documented commands, switching between verbose or traditional output.""" import io @@ -4103,14 +4107,14 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose self.poutput(category_grid, soft_wrap=False) self.poutput() - def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> str: + def render_columns(self, str_list: Sequence[str] | None, display_width: int = 80) -> str: """Render a list of single-line strings as a compact set of columns. This method correctly handles strings containing ANSI style sequences and full-width characters (like those used in CJK languages). Each column is only as wide as necessary and columns are separated by two spaces. - :param str_list: list of single-line strings to display + :param str_list: Sequence of single-line strings to display :param display_width: max number of display columns to fit into :return: a string containing the columnized output """ @@ -4162,14 +4166,14 @@ def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> return "\n".join(rows) - def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None: + def columnize(self, str_list: Sequence[str] | None, display_width: int = 80) -> None: """Display a list of single-line strings as a compact set of columns. Override of cmd's columnize() that uses the render_columns() method. The method correctly handles strings with ANSI style sequences and full-width characters (like those used in CJK languages). - :param str_list: list of single-line strings to display + :param str_list: Sequence of single-line strings to display :param display_width: max number of display columns to fit into """ columnized_strs = self.render_columns(str_list, display_width) @@ -4220,7 +4224,7 @@ def do_quit(self, _: argparse.Namespace) -> bool | None: self.last_result = True return True - def select(self, opts: str | list[str] | list[tuple[Any, str | None]], prompt: str = 'Your choice? ') -> Any: + def select(self, opts: str | Iterable[str] | Iterable[tuple[Any, str | None]], prompt: str = 'Your choice? ') -> Any: """Present a numbered menu to the user. Modeled after the bash shell's SELECT. Returns the item chosen. @@ -4233,7 +4237,7 @@ def select(self, opts: str | list[str] | list[tuple[Any, str | None]], prompt: s that the return value can differ from the text advertised to the user """ - local_opts: list[str] | list[tuple[Any, str | None]] + local_opts: Iterable[str] | Iterable[tuple[Any, str | None]] if isinstance(opts, str): local_opts = cast(list[tuple[Any, str | None]], list(zip(opts.split(), opts.split(), strict=False))) else: @@ -4295,7 +4299,7 @@ def _build_base_set_parser(cls) -> Cmd2ArgumentParser: return base_set_parser def complete_set_value( - self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]] + self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Mapping[str, Sequence[str]] ) -> Completions: """Completes the value argument of set.""" param = arg_tokens['param'][0] diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 52682608..d7a1c508 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -354,7 +354,7 @@ def as_subcommand_to( | Callable[[CommandParentClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod *, help: str | None = None, # noqa: A002 - aliases: list[str] | None = None, + aliases: Sequence[str] | None = None, ) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: """Tag this method as a subcommand to an existing argparse decorated command. diff --git a/cmd2/parsing.py b/cmd2/parsing.py index e1095529..b0f059c5 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -5,6 +5,7 @@ import sys from collections.abc import ( Iterable, + Mapping, Sequence, ) from dataclasses import ( @@ -284,8 +285,8 @@ def __init__( self, terminators: Iterable[str] | None = None, multiline_commands: Iterable[str] | None = None, - aliases: dict[str, str] | None = None, - shortcuts: dict[str, str] | None = None, + aliases: Mapping[str, str] | None = None, + shortcuts: Mapping[str, str] | None = None, ) -> None: """Initialize an instance of StatementParser. @@ -303,7 +304,7 @@ def __init__( else: self.terminators = tuple(terminators) self.multiline_commands: tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else () - self.aliases: dict[str, str] = aliases if aliases is not None else {} + self.aliases: dict[str, str] = dict(aliases) if aliases is not None else {} if shortcuts is None: shortcuts = constants.DEFAULT_SHORTCUTS diff --git a/cmd2/utils.py b/cmd2/utils.py index 342dedec..d698b4eb 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -14,6 +14,7 @@ from collections.abc import ( Callable, Iterable, + MutableSequence, ) from difflib import SequenceMatcher from enum import Enum @@ -247,7 +248,7 @@ def natural_sort(list_to_sort: Iterable[str]) -> list[str]: return sorted(list_to_sort, key=natural_keys) -def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None: +def quote_specific_tokens(tokens: MutableSequence[str], tokens_to_quote: Iterable[str]) -> None: """Quote specific tokens in a list. :param tokens: token list being edited @@ -258,7 +259,7 @@ def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None tokens[i] = su.quote(token) -def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> None: +def unquote_specific_tokens(tokens: MutableSequence[str], tokens_to_unquote: Iterable[str]) -> None: """Unquote specific tokens in a list. :param tokens: token list being edited @@ -291,7 +292,7 @@ def expand_user(token: str) -> str: return token -def expand_user_in_tokens(tokens: list[str]) -> None: +def expand_user_in_tokens(tokens: MutableSequence[str]) -> None: """Call expand_user() on all tokens in a list of strings. :param tokens: tokens to expand. @@ -344,12 +345,12 @@ def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> list[str]: return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)] -def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list[str]: +def files_from_glob_patterns(patterns: Iterable[str], access: int = os.F_OK) -> list[str]: """Return a list of file paths based on a list of glob patterns. Only files are returned, not directories, and optionally only files for which the user has a specified access to. - :param patterns: list of file names and/or glob patterns + :param patterns: Iterable of file names and/or glob patterns :param access: file access type to verify (os.* where * is F_OK, R_OK, W_OK, or X_OK) :return: list of files matching the names and/or glob patterns """ From 1812cfee10c0ea200f6e4d1bd5ef236a7a39f9c1 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 21 Feb 2026 19:34:47 -0500 Subject: [PATCH 2/3] Made the ArgTokens type alias consistent with the cmd2.Cmd methods using it --- cmd2/completion.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd2/completion.py b/cmd2/completion.py index d6e1afe9..dd67c096 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -7,6 +7,7 @@ Collection, Iterable, Iterator, + Mapping, Sequence, ) from dataclasses import ( @@ -270,7 +271,7 @@ def all_display_numeric(items: Collection[CompletionItem]) -> bool: ############################################# # Represents the parsed tokens from argparse during completion -ArgTokens: TypeAlias = dict[str, list[str]] +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. From 74dc041f990a3e4637b4c233627af894a4477da7 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 21 Feb 2026 19:45:47 -0500 Subject: [PATCH 3/3] Modernize build requirements This is mostly an attempt to fix a transient failure in a PR GitHub Actions pipeline. --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 96042a5e..20daa022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["build>=1.2.2", "setuptools>=80.7.1", "setuptools-scm>=9.2"] +requires = ["build>=1.3.0", "setuptools>=80.8.0", "setuptools-scm>=9.2.1"] build-backend = "setuptools.build_meta" [project] @@ -38,7 +38,7 @@ dependencies = [ ] [dependency-groups] -build = ["build>=1.2.2", "setuptools>=80.7.1", "setuptools-scm>=9.2"] +build = ["build>=1.3.0", "setuptools>=80.8.0", "setuptools-scm>=9.2.1"] dev = [ "codecov>=2.1", "ipython>=8.23", @@ -54,7 +54,7 @@ dev = [ ] docs = [ "mkdocstrings[python]>=1", - "setuptools>=80.7.1", + "setuptools>=80.8.0", "setuptools_scm>=8", "zensical>=0.0.17", ] @@ -66,7 +66,7 @@ test = [ "pytest-cov>=5", "pytest-mock>=3.14.1", ] -validate = ["mypy>=1.13", "ruff>=0.14.10", "types-setuptools>=80.7.1"] +validate = ["mypy>=1.13", "ruff>=0.14.10", "types-setuptools>=80.8.0"] [tool.mypy] disallow_incomplete_defs = true