Skip to content

Commit 3ee5e93

Browse files
committed
Moved decorators._set_parser_prog() to argparse_custom.set_parser_prog().
1 parent dd46c35 commit 3ee5e93

File tree

5 files changed

+54
-56
lines changed

5 files changed

+54
-56
lines changed

cmd2/argparse_custom.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,56 @@ def generate_range_error(range_min: int, range_max: float) -> str:
291291
return err_str
292292

293293

294+
def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
295+
"""Recursively set prog attribute of a parser and all of its subparsers.
296+
297+
Does so that the root command is a command name and not sys.argv[0].
298+
299+
:param parser: the parser being edited
300+
:param prog: new value for the parser's prog attribute
301+
"""
302+
# Set the prog value for this parser
303+
parser.prog = prog
304+
req_args: list[str] = []
305+
306+
# Set the prog value for the parser's subcommands
307+
for action in parser._actions:
308+
if isinstance(action, argparse._SubParsersAction):
309+
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
310+
# the correct prog value will be set on the parser being added.
311+
action._prog_prefix = parser.prog
312+
313+
# The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
314+
# same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
315+
# Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
316+
# we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
317+
# help information and names for the subcommands and not aliases. However, subcommands without help text
318+
# won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
319+
# subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
320+
# parser, the dictionary key is a subcommand and not alias.
321+
processed_parsers = []
322+
323+
# Set the prog value for each subcommand's parser
324+
for subcmd_name, subcmd_parser in action.choices.items():
325+
# Check if we've already edited this parser
326+
if subcmd_parser in processed_parsers:
327+
continue
328+
329+
subcmd_prog = parser.prog
330+
if req_args:
331+
subcmd_prog += " " + " ".join(req_args)
332+
subcmd_prog += " " + subcmd_name
333+
set_parser_prog(subcmd_parser, subcmd_prog)
334+
processed_parsers.append(subcmd_parser)
335+
336+
# We can break since argparse only allows 1 group of subcommands per level
337+
break
338+
339+
# Need to save required args so they can be prepended to the subcommand usage
340+
if action.required:
341+
req_args.append(action.dest)
342+
343+
294344
class CompletionItem(str): # noqa: SLOT000
295345
"""Completion item with descriptive text attached.
296346

cmd2/cmd2.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -784,9 +784,7 @@ def _build_parser(
784784
else:
785785
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")
786786

787-
from .decorators import _set_parser_prog
788-
789-
_set_parser_prog(parser, prog)
787+
argparse_custom.set_parser_prog(parser, prog)
790788

791789
return parser
792790

cmd2/decorators.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -192,56 +192,6 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]:
192192
return arg_decorator
193193

194194

195-
def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
196-
"""Recursively set prog attribute of a parser and all of its subparsers.
197-
198-
Does so that the root command is a command name and not sys.argv[0].
199-
200-
:param parser: the parser being edited
201-
:param prog: new value for the parser's prog attribute
202-
"""
203-
# Set the prog value for this parser
204-
parser.prog = prog
205-
req_args: list[str] = []
206-
207-
# Set the prog value for the parser's subcommands
208-
for action in parser._actions:
209-
if isinstance(action, argparse._SubParsersAction):
210-
# Set the _SubParsersAction's _prog_prefix value. That way if its add_parser() method is called later,
211-
# the correct prog value will be set on the parser being added.
212-
action._prog_prefix = parser.prog
213-
214-
# The keys of action.choices are subcommand names as well as subcommand aliases. The aliases point to the
215-
# same parser as the actual subcommand. We want to avoid placing an alias into a parser's prog value.
216-
# Unfortunately there is nothing about an action.choices entry which tells us it's an alias. In most cases
217-
# we can filter out the aliases by checking the contents of action._choices_actions. This list only contains
218-
# help information and names for the subcommands and not aliases. However, subcommands without help text
219-
# won't show up in that list. Since dictionaries are ordered in Python 3.6 and above and argparse inserts the
220-
# subcommand name into choices dictionary before aliases, we should be OK assuming the first time we see a
221-
# parser, the dictionary key is a subcommand and not alias.
222-
processed_parsers = []
223-
224-
# Set the prog value for each subcommand's parser
225-
for subcmd_name, subcmd_parser in action.choices.items():
226-
# Check if we've already edited this parser
227-
if subcmd_parser in processed_parsers:
228-
continue
229-
230-
subcmd_prog = parser.prog
231-
if req_args:
232-
subcmd_prog += " " + " ".join(req_args)
233-
subcmd_prog += " " + subcmd_name
234-
_set_parser_prog(subcmd_parser, subcmd_prog)
235-
processed_parsers.append(subcmd_parser)
236-
237-
# We can break since argparse only allows 1 group of subcommands per level
238-
break
239-
240-
# Need to save required args so they can be prepended to the subcommand usage
241-
if action.required:
242-
req_args.append(action.dest)
243-
244-
245195
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
246196
#: and optionally return a boolean
247197
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]

tests/test_argparse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def base_helpless(self, args) -> None:
290290
parser_bar.set_defaults(func=base_bar)
291291

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

402402

403403
def test_add_another_subcommand(subcommand_app) -> None:
404-
"""This tests makes sure _set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
404+
"""This tests makes sure set_parser_prog() sets _prog_prefix on every _SubParsersAction so that all future calls
405405
to add_parser() write the correct prog value to the parser being added.
406406
"""
407407
base_parser = subcommand_app._command_parsers.get(subcommand_app.do_base)

tests_isolated/test_commandset/test_argparse_subcommands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def base_helpless(self, args) -> None:
4545
parser_bar.set_defaults(func=base_bar)
4646

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

0 commit comments

Comments
 (0)