From a051b29a7240a52155dd319967b8892886401545 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 Apr 2026 10:31:56 -0400 Subject: [PATCH 1/2] Fixed ArgparseCompleter.print_help() not passing file stream to recursive call. --- CHANGELOG.md | 6 ++++++ cmd2/argparse_completer.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53590b36f..b5e563151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,12 @@ prompt is displayed. the property. - Added `traceback_kwargs` attribute to allow customization of Rich-based tracebacks. +## 3.5.1 (April 24, 2026) + +- Bug Fixes + - Fixed `ArgparseCompleter.print_help()` not passing file stream to recursive call. + - Fixed issue where `constants.REDIRECTION_TOKENS` was being mutated. + ## 3.5.0 (April 13, 2026) - Bug Fixes diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index ec1dcdd02..9f07162f6 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -705,7 +705,7 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None if parser is not None: completer_type = self._cmd2_app._determine_ap_completer_type(parser) completer = completer_type(parser, self._cmd2_app) - completer.print_help(tokens[1:]) + completer.print_help(tokens[1:], file=file) return self._parser.print_help(file=file) From b7376b3c14fcde2c2a7d53ada2be6ee950a44277 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 Apr 2026 11:30:00 -0400 Subject: [PATCH 2/2] Fixed issue where constants.REDIRECTION_TOKENS was being mutated. --- cmd2/cmd2.py | 20 ++++++-------------- cmd2/constants.py | 6 +++--- cmd2/parsing.py | 20 ++++++++++---------- cmd2/string_utils.py | 4 +--- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f3f4e3da5..f2ab98944 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1976,10 +1976,8 @@ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[li **On Failure** - Two empty lists """ - import copy - unclosed_quote = "" - quotes_to_try = copy.copy(constants.QUOTES) + quotes_to_try = [*constants.QUOTES] tmp_line = line[:endidx] tmp_endidx = endidx @@ -3818,8 +3816,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: return # Unquote redirection and terminator tokens - tokens_to_unquote = constants.REDIRECTION_TOKENS - tokens_to_unquote.extend(self.statement_parser.terminators) + tokens_to_unquote = (*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators) utils.unquote_specific_tokens(args.command_args, tokens_to_unquote) # Build the alias value string @@ -3898,8 +3895,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases as 'alias create' commands.""" self.last_result = {} # dict[alias_name, alias_value] - tokens_to_quote = constants.REDIRECTION_TOKENS - tokens_to_quote.extend(self.statement_parser.terminators) + tokens_to_quote = (*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators) to_list = ( utils.remove_duplicates(args.names) @@ -4065,8 +4061,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: return # Unquote redirection and terminator tokens - tokens_to_unquote = constants.REDIRECTION_TOKENS - tokens_to_unquote.extend(self.statement_parser.terminators) + tokens_to_unquote = (*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators) utils.unquote_specific_tokens(args.command_args, tokens_to_unquote) # Build the macro value string @@ -4188,8 +4183,7 @@ def _macro_list(self, args: argparse.Namespace) -> None: """List macros.""" self.last_result = {} # dict[macro_name, macro_value] - tokens_to_quote = constants.REDIRECTION_TOKENS - tokens_to_quote.extend(self.statement_parser.terminators) + tokens_to_quote = (*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators) to_list = ( utils.remove_duplicates(args.names) @@ -4917,9 +4911,7 @@ def py_quit() -> None: """Exit an interactive Python environment, callable from the interactive Python console.""" raise EmbeddedConsoleExit - from .py_bridge import ( - PyBridge, - ) + from .py_bridge import PyBridge add_to_history = self.scripts_add_to_history if pyscript else True py_bridge = PyBridge(self, add_to_history=add_to_history) diff --git a/cmd2/constants.py b/cmd2/constants.py index dc92be4b5..34f927f74 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -6,12 +6,12 @@ INFINITY = float("inf") # Used for command parsing, output redirection, completion, and word breaks. Do not change. -QUOTES = ['"', "'"] +QUOTES = ('"', "'") REDIRECTION_PIPE = "|" REDIRECTION_OVERWRITE = ">" REDIRECTION_APPEND = ">>" -REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OVERWRITE] -REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OVERWRITE, REDIRECTION_APPEND] +REDIRECTION_CHARS = (REDIRECTION_PIPE, REDIRECTION_OVERWRITE) +REDIRECTION_TOKENS = (REDIRECTION_PIPE, REDIRECTION_OVERWRITE, REDIRECTION_APPEND) COMMENT_CHAR = "#" MULTILINE_TERMINATOR = ";" diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 6a4a75c7b..6df8511f6 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -330,10 +330,12 @@ def __init__( # the string (\Z matches the end of the string even if it # contains multiple lines) # - invalid_command_chars = [] - invalid_command_chars.extend(constants.QUOTES) - invalid_command_chars.extend(constants.REDIRECTION_CHARS) - invalid_command_chars.extend(self.terminators) + invalid_command_chars = ( + *constants.QUOTES, + *constants.REDIRECTION_CHARS, + *self.terminators, + ) + # escape each item so it will for sure get treated as a literal second_group_items = [re.escape(x) for x in invalid_command_chars] # add the whitespace and end of string, not escaped because they @@ -384,9 +386,8 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[b return False, errmsg errmsg = "cannot contain: whitespace, quotes, " - errchars = [] - errchars.extend(constants.REDIRECTION_CHARS) - errchars.extend(self.terminators) + + errchars = (*constants.REDIRECTION_CHARS, *self.terminators) errmsg += ", ".join([shlex.quote(x) for x in errchars]) match = self._command_pattern.search(word) @@ -704,9 +705,8 @@ def split_on_punctuation(self, tokens: list[str]) -> list[str]: :param tokens: the tokens as parsed by shlex :return: a new list of tokens, further split using punctuation """ - punctuation: list[str] = [] - punctuation.extend(self.terminators) - punctuation.extend(constants.REDIRECTION_CHARS) + # Using a set for faster lookups + punctuation = {*self.terminators, *constants.REDIRECTION_CHARS} punctuated_tokens = [] diff --git a/cmd2/string_utils.py b/cmd2/string_utils.py index acb4ee347..c2af4d00a 100644 --- a/cmd2/string_utils.py +++ b/cmd2/string_utils.py @@ -5,9 +5,7 @@ full-width characters (like those used in CJK languages). """ -from collections.abc import ( - Sequence, -) +from collections.abc import Sequence from rich.align import AlignMethod from rich.style import StyleType