Skip to content

Moved decorators._set_parser_prog() to argparse_custom.set_parser_prog(). #1466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 20, 2025
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
50 changes: 50 additions & 0 deletions cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,56 @@ def generate_range_error(range_min: int, range_max: float) -> str:
return err_str


def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
"""Recursively set prog attribute of a parser and all of its subparsers.

Does so that the root command is a command name and not sys.argv[0].

:param parser: the parser being edited
:param prog: new value for the parser's prog attribute
"""
# Set the prog value for this parser
parser.prog = prog
req_args: list[str] = []

# Set the prog value for the parser's subcommands
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
# the correct prog value will be set on the parser being added.
action._prog_prefix = parser.prog

# The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
# same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
# Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
# we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
# help information and names for the subcommands and not aliases. However, subcommands without help text
# won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
# subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
# parser, the dictionary key is a subcommand and not alias.
processed_parsers = []

# Set the prog value for each subcommand's parser
for subcmd_name, subcmd_parser in action.choices.items():
# Check if we've already edited this parser
if subcmd_parser in processed_parsers:
continue

subcmd_prog = parser.prog
if req_args:
subcmd_prog += " " + " ".join(req_args)
subcmd_prog += " " + subcmd_name
set_parser_prog(subcmd_parser, subcmd_prog)
processed_parsers.append(subcmd_parser)

# We can break since argparse only allows 1 group of subcommands per level
break

# Need to save required args so they can be prepended to the subcommand usage
if action.required:
req_args.append(action.dest)


class CompletionItem(str): # noqa: SLOT000
"""Completion item with descriptive text attached.

Expand Down
4 changes: 1 addition & 3 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,7 @@ def _build_parser(
else:
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")

from .decorators import _set_parser_prog

_set_parser_prog(parser, prog)
argparse_custom.set_parser_prog(parser, prog)

return parser

Expand Down
50 changes: 0 additions & 50 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,56 +192,6 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]:
return arg_decorator


def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
"""Recursively set prog attribute of a parser and all of its subparsers.

Does so that the root command is a command name and not sys.argv[0].

:param parser: the parser being edited
:param prog: new value for the parser's prog attribute
"""
# Set the prog value for this parser
parser.prog = prog
req_args: list[str] = []

# Set the prog value for the parser's subcommands
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
# the correct prog value will be set on the parser being added.
action._prog_prefix = parser.prog

# The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
# same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
# Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
# we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
# help information and names for the subcommands and not aliases. However, subcommands without help text
# won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
# subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
# parser, the dictionary key is a subcommand and not alias.
processed_parsers = []

# Set the prog value for each subcommand's parser
for subcmd_name, subcmd_parser in action.choices.items():
# Check if we've already edited this parser
if subcmd_parser in processed_parsers:
continue

subcmd_prog = parser.prog
if req_args:
subcmd_prog += " " + " ".join(req_args)
subcmd_prog += " " + subcmd_name
_set_parser_prog(subcmd_parser, subcmd_prog)
processed_parsers.append(subcmd_parser)

# We can break since argparse only allows 1 group of subcommands per level
break

# Need to save required args so they can be prepended to the subcommand usage
if action.required:
req_args.append(action.dest)


#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and optionally return a boolean
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
Expand Down
4 changes: 2 additions & 2 deletions tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def base_helpless(self, args) -> None:
parser_bar.set_defaults(func=base_bar)

# create the parser for the "helpless" subcommand
# This subcommand has aliases and no help text. It exists to prevent changes to _set_parser_prog() which
# This subcommand has aliases and no help text. It exists to prevent changes to set_parser_prog() which
# use an approach which relies on action._choices_actions list. See comment in that function for more
# details.
parser_helpless = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2'])
Expand Down Expand Up @@ -401,7 +401,7 @@ def test_subcommand_invalid_help(subcommand_app) -> None:


def test_add_another_subcommand(subcommand_app) -> None:
"""This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
"""This tests makes sure set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
to add_parser() write the correct prog value to the parser being added.
"""
base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def base_helpless(self, args) -> None:
parser_bar.set_defaults(func=base_bar)

# create the parser for the "helpless" subcommand
# This subcommand has aliases and no help text. It exists to prevent changes to _set_parser_prog() which
# This subcommand has aliases and no help text. It exists to prevent changes to set_parser_prog() which
# use an approach which relies on action._choices_actions list. See comment in that function for more
# details.
parser_helpless = base_subparsers.add_parser('helpless', aliases=['helpless_1', 'helpless_2'])
Expand Down
Loading