Skip to content

Commit

Permalink
pythongh-86463: Fix default prog in subparsers if usage is used in th…
Browse files Browse the repository at this point in the history
…e parent parser

The placeholder '...' is now used as the prog prefix in subparsers
if a custom usage is specified in the parent parser and prog is not
specified in the subparser.

Previously the full custom usage of the parent parser was used as
the prog prefix in subparsers.
  • Loading branch information
serhiy-storchaka committed Sep 25, 2024
1 parent 78aeb38 commit f7d939b
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ The default message can be overridden with the ``usage=`` keyword argument::
The ``%(prog)s`` format specifier is available to fill in the program name in
your usage messages.

When you specify custom usage, always specify also prog_ and usage_
arguments to :meth:`subparsers <_SubParsersAction.add_parser>`.


.. _description:

Expand Down
13 changes: 8 additions & 5 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1813,11 +1813,14 @@ def add_subparsers(self, **kwargs):
# prog defaults to the usage message of this parser, skipping
# optional arguments and with no "usage:" prefix
if kwargs.get('prog') is None:
formatter = self._get_formatter()
positionals = self._get_positional_actions()
groups = self._mutually_exclusive_groups
formatter.add_usage(self.usage, positionals, groups, '')
kwargs['prog'] = formatter.format_help().strip()
if self.usage is None:
formatter = self._get_formatter()
positionals = self._get_positional_actions()
groups = self._mutually_exclusive_groups
formatter.add_usage(self.usage, positionals, groups, '')
kwargs['prog'] = formatter.format_help().strip()
else:
kwargs['prog'] = '...'

# create the parsers action and add it to the positionals list
parsers_class = self._pop_action_class(kwargs, 'parsers')
Expand Down
51 changes: 47 additions & 4 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2203,16 +2203,17 @@ def assertArgumentParserError(self, *args, **kwargs):
self.assertRaises(ArgumentParserError, *args, **kwargs)

def _get_parser(self, subparser_help=False, prefix_chars=None,
aliases=False):
aliases=False, usage=None):
# create a parser with a subparsers argument
if prefix_chars:
parser = ErrorRaisingArgumentParser(
prog='PROG', description='main description', prefix_chars=prefix_chars)
prog='PROG', description='main description', usage=usage,
prefix_chars=prefix_chars)
parser.add_argument(
prefix_chars[0] * 2 + 'foo', action='store_true', help='foo help')
else:
parser = ErrorRaisingArgumentParser(
prog='PROG', description='main description')
prog='PROG', description='main description', usage=usage)
parser.add_argument(
'--foo', action='store_true', help='foo help')
parser.add_argument(
Expand Down Expand Up @@ -2249,7 +2250,8 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
parser2.add_argument('z', type=complex, nargs='*', help='z help')

# add third sub-parser
parser3_kwargs = dict(description='3 description')
parser3_kwargs = dict(description='3 description',
usage='PROG --foo bar 3 t ...')
if subparser_help:
parser3_kwargs['help'] = '3 help'
parser3 = subparsers.add_parser('3', **parser3_kwargs)
Expand All @@ -2271,6 +2273,47 @@ def test_parse_args_failures(self):
args = args_str.split()
self.assertArgumentParserError(self.parser.parse_args, args)

def test_parse_args_failures_details(self):
for args_str, usage_str, error_str in [
('',
'usage: PROG [-h] [--foo] bar {1,2,3} ...',
'PROG: error: the following arguments are required: bar'),
('0.5 1 -y',
'usage: PROG bar 1 [-h] [-w W] {a,b,c}',
'PROG bar 1: error: the following arguments are required: x'),
('0.5 3',
'usage: PROG --foo bar 3 t ...',
'PROG bar 3: error: the following arguments are required: t'),
]:
with self.subTest(args_str):
args = args_str.split()
with self.assertRaises(ArgumentParserError) as cm:
self.parser.parse_args(args)
self.assertEqual(cm.exception.args[0], 'SystemExit')
self.assertEqual(cm.exception.args[2], f'{usage_str}\n{error_str}\n')

def test_parse_args_failures_details_custom_usage(self):
parser = self._get_parser(usage='PROG [--foo] bar 1 [-w W] {a,b,c}\n'
' PROG --foo bar 3 t ...')
for args_str, usage_str, error_str in [
('',
'usage: PROG [--foo] bar 1 [-w W] {a,b,c}\n'
' PROG --foo bar 3 t ...',
'PROG: error: the following arguments are required: bar'),
('0.5 1 -y',
'usage: ... 1 [-h] [-w W] {a,b,c}',
'... 1: error: the following arguments are required: x'),
('0.5 3',
'usage: PROG --foo bar 3 t ...',
'... 3: error: the following arguments are required: t'),
]:
with self.subTest(args_str):
args = args_str.split()
with self.assertRaises(ArgumentParserError) as cm:
parser.parse_args(args)
self.assertEqual(cm.exception.args[0], 'SystemExit')
self.assertEqual(cm.exception.args[2], f'{usage_str}\n{error_str}\n')

def test_parse_args(self):
# check some non-failure cases:
self.assertEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`argparse` now uses the placeholder ``'...'`` as the ``prog`` prefix in
subparsers if a custom ``usage`` is specified in the parent parser and
``prog`` is not specified in the subparser.

0 comments on commit f7d939b

Please sign in to comment.