Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
## 2.6.2 (TBD, 2025)

- Enhancements

- Added explicit support for free-threaded versions of Python, starting with version 3.14

- Bug Fixes
- Restored code to set a parser's `prog` value in the `with_argparser` decorator. This is to
preserve backward compatibility in the `cmd2` 2.0 family. This functionality will be removed
in `cmd2` 3.0.0.

## 2.6.1 (June 8, 2025)

- Bug Fixes
Expand Down
54 changes: 29 additions & 25 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,17 +254,11 @@ def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
command = command_method.__name__[len(COMMAND_FUNC_PREFIX) :]

parser_builder = getattr(command_method, constants.CMD_ATTR_ARGPARSER, None)
parent = self._cmd.find_commandset_for_command(command) or self._cmd
parser = self._cmd._build_parser(parent, parser_builder)
if parser is None:
if parser_builder is None:
return None

# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
from .decorators import (
_set_parser_prog,
)

_set_parser_prog(parser, command)
parent = self._cmd.find_commandset_for_command(command) or self._cmd
parser = self._cmd._build_parser(parent, parser_builder, command)

# If the description has not been set, then use the method docstring if one exists
if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
Expand Down Expand Up @@ -758,24 +752,39 @@ def register_command_set(self, cmdset: CommandSet) -> None:
def _build_parser(
self,
parent: CommandParent,
parser_builder: Optional[
Union[
argparse.ArgumentParser,
Callable[[], argparse.ArgumentParser],
StaticArgParseBuilder,
ClassArgParseBuilder,
]
parser_builder: Union[
argparse.ArgumentParser,
Callable[[], argparse.ArgumentParser],
StaticArgParseBuilder,
ClassArgParseBuilder,
],
) -> Optional[argparse.ArgumentParser]:
parser: Optional[argparse.ArgumentParser] = None
prog: str,
) -> argparse.ArgumentParser:
"""Build argument parser for a command/subcommand.

:param parent: CommandParent object which owns the parser
:param parser_builder: method used to build the parser
:param prog: prog value to set in new parser
:return: new parser
:raises TypeError: if parser_builder is invalid type
"""
if isinstance(parser_builder, staticmethod):
parser = parser_builder.__func__()
elif isinstance(parser_builder, classmethod):
parser = parser_builder.__func__(parent if not None else self) # type: ignore[arg-type]
parser = parser_builder.__func__(parent.__class__)
elif callable(parser_builder):
parser = parser_builder()
elif isinstance(parser_builder, argparse.ArgumentParser):
parser = copy.deepcopy(parser_builder)
else:
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")

from .decorators import (
_set_parser_prog,
)

_set_parser_prog(parser, prog)

return parser

def _install_command_function(self, command_func_name: str, command_method: CommandFunc, context: str = '') -> None:
Expand Down Expand Up @@ -963,12 +972,7 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) ->

target_parser = find_subcommand(command_parser, subcommand_names)

subcmd_parser = cast(argparse.ArgumentParser, self._build_parser(cmdset, subcmd_parser_builder))
from .decorators import (
_set_parser_prog,
)

_set_parser_prog(subcmd_parser, f'{command_name} {subcommand_name}')
subcmd_parser = self._build_parser(cmdset, subcmd_parser_builder, f'{command_name} {subcommand_name}')
if subcmd_parser.description is None and method.__doc__:
subcmd_parser.description = strip_doc_annotations(method.__doc__)

Expand Down
8 changes: 8 additions & 0 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]:

command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]

if isinstance(parser, argparse.ArgumentParser):
# Set parser's prog value for backward compatibility within the cmd2 2.0 family.
# This will be removed in cmd2 3.0 since we never reference this parser object's prog value.
# Since it's possible for the same parser object to be passed into multiple with_argparser()
# calls, we only set prog on the deep copies of this parser based on the specific do_xxxx
# instance method they are associated with.
_set_parser_prog(parser, command_name)

# Set some custom attributes for this command
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
Expand Down
6 changes: 6 additions & 0 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ def test_preservelist(argparse_app) -> None:
assert out[0] == "['foo', '\"bar baz\"']"


def test_invalid_parser_builder(argparse_app):
parser_builder = None
with pytest.raises(TypeError):
argparse_app._build_parser(argparse_app, parser_builder, "fake_prog")


def _build_has_subcmd_parser() -> cmd2.Cmd2ArgumentParser:
has_subcmds_parser = cmd2.Cmd2ArgumentParser(description="Tests as_subcmd_to decorator")
has_subcmds_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND', required=True)
Expand Down
Loading