Skip to content

Commit 0cb4d6c

Browse files
gh-86463: Fix default prog in subparsers if usage is used in the main parser (GH-125891)
The usage parameter of argparse.ArgumentParser no longer affects the default value of the prog parameter in subparsers. Previously the full custom usage of the main parser was used as the prog prefix in subparsers.
1 parent 46f8a7b commit 0cb4d6c

File tree

4 files changed

+60
-5
lines changed

4 files changed

+60
-5
lines changed

Doc/library/argparse.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ arguments it contains. The default message can be overridden with the
192192
The ``%(prog)s`` format specifier is available to fill in the program name in
193193
your usage messages.
194194

195+
When a custom usage message is specified for the main parser, you may also want to
196+
consider passing the ``prog`` argument to :meth:`~ArgumentParser.add_subparsers`
197+
or the ``prog`` and the ``usage`` arguments to
198+
:meth:`~_SubParsersAction.add_parser`, to ensure consistent command prefixes and
199+
usage information across subparsers.
200+
195201

196202
.. _description:
197203

@@ -1810,6 +1816,10 @@ Sub-commands
18101816
.. versionchanged:: 3.7
18111817
New *required* keyword-only parameter.
18121818

1819+
.. versionchanged:: 3.14
1820+
Subparser's *prog* is no longer affected by a custom usage message in
1821+
the main parser.
1822+
18131823

18141824
FileType objects
18151825
^^^^^^^^^^^^^^^^

Lib/argparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1889,7 +1889,7 @@ def add_subparsers(self, **kwargs):
18891889
formatter = self._get_formatter()
18901890
positionals = self._get_positional_actions()
18911891
groups = self._mutually_exclusive_groups
1892-
formatter.add_usage(self.usage, positionals, groups, '')
1892+
formatter.add_usage(None, positionals, groups, '')
18931893
kwargs['prog'] = formatter.format_help().strip()
18941894

18951895
# create the parsers action and add it to the positionals list

Lib/test/test_argparse.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,16 +2409,17 @@ def assertArgumentParserError(self, *args, **kwargs):
24092409
self.assertRaises(ArgumentParserError, *args, **kwargs)
24102410

24112411
def _get_parser(self, subparser_help=False, prefix_chars=None,
2412-
aliases=False):
2412+
aliases=False, usage=None):
24132413
# create a parser with a subparsers argument
24142414
if prefix_chars:
24152415
parser = ErrorRaisingArgumentParser(
2416-
prog='PROG', description='main description', prefix_chars=prefix_chars)
2416+
prog='PROG', description='main description', usage=usage,
2417+
prefix_chars=prefix_chars)
24172418
parser.add_argument(
24182419
prefix_chars[0] * 2 + 'foo', action='store_true', help='foo help')
24192420
else:
24202421
parser = ErrorRaisingArgumentParser(
2421-
prog='PROG', description='main description')
2422+
prog='PROG', description='main description', usage=usage)
24222423
parser.add_argument(
24232424
'--foo', action='store_true', help='foo help')
24242425
parser.add_argument(
@@ -2455,7 +2456,8 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
24552456
parser2.add_argument('z', type=complex, nargs='*', help='z help')
24562457

24572458
# add third sub-parser
2458-
parser3_kwargs = dict(description='3 description')
2459+
parser3_kwargs = dict(description='3 description',
2460+
usage='PROG --foo bar 3 t ...')
24592461
if subparser_help:
24602462
parser3_kwargs['help'] = '3 help'
24612463
parser3 = subparsers.add_parser('3', **parser3_kwargs)
@@ -2477,6 +2479,47 @@ def test_parse_args_failures(self):
24772479
args = args_str.split()
24782480
self.assertArgumentParserError(self.parser.parse_args, args)
24792481

2482+
def test_parse_args_failures_details(self):
2483+
for args_str, usage_str, error_str in [
2484+
('',
2485+
'usage: PROG [-h] [--foo] bar {1,2,3} ...',
2486+
'PROG: error: the following arguments are required: bar'),
2487+
('0.5 1 -y',
2488+
'usage: PROG bar 1 [-h] [-w W] {a,b,c}',
2489+
'PROG bar 1: error: the following arguments are required: x'),
2490+
('0.5 3',
2491+
'usage: PROG --foo bar 3 t ...',
2492+
'PROG bar 3: error: the following arguments are required: t'),
2493+
]:
2494+
with self.subTest(args_str):
2495+
args = args_str.split()
2496+
with self.assertRaises(ArgumentParserError) as cm:
2497+
self.parser.parse_args(args)
2498+
self.assertEqual(cm.exception.args[0], 'SystemExit')
2499+
self.assertEqual(cm.exception.args[2], f'{usage_str}\n{error_str}\n')
2500+
2501+
def test_parse_args_failures_details_custom_usage(self):
2502+
parser = self._get_parser(usage='PROG [--foo] bar 1 [-w W] {a,b,c}\n'
2503+
' PROG --foo bar 3 t ...')
2504+
for args_str, usage_str, error_str in [
2505+
('',
2506+
'usage: PROG [--foo] bar 1 [-w W] {a,b,c}\n'
2507+
' PROG --foo bar 3 t ...',
2508+
'PROG: error: the following arguments are required: bar'),
2509+
('0.5 1 -y',
2510+
'usage: PROG bar 1 [-h] [-w W] {a,b,c}',
2511+
'PROG bar 1: error: the following arguments are required: x'),
2512+
('0.5 3',
2513+
'usage: PROG --foo bar 3 t ...',
2514+
'PROG bar 3: error: the following arguments are required: t'),
2515+
]:
2516+
with self.subTest(args_str):
2517+
args = args_str.split()
2518+
with self.assertRaises(ArgumentParserError) as cm:
2519+
parser.parse_args(args)
2520+
self.assertEqual(cm.exception.args[0], 'SystemExit')
2521+
self.assertEqual(cm.exception.args[2], f'{usage_str}\n{error_str}\n')
2522+
24802523
def test_parse_args(self):
24812524
# check some non-failure cases:
24822525
self.assertEqual(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The ``usage`` parameter of :class:`argparse.ArgumentParser` no longer
2+
affects the default value of the ``prog`` parameter in subparsers.

0 commit comments

Comments
 (0)