diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 53ecc97d5659f4..09982854948f95 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -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: diff --git a/Lib/argparse.py b/Lib/argparse.py index 690b2a9db9481b..d559ab5e7ed495 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -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') diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index ef05a6fefcffcc..865fb77d997f1d 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -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( @@ -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) @@ -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( diff --git a/Misc/NEWS.d/next/Library/2024-09-25-21-27-37.gh-issue-86463.lcBmsO.rst b/Misc/NEWS.d/next/Library/2024-09-25-21-27-37.gh-issue-86463.lcBmsO.rst new file mode 100644 index 00000000000000..98fd50f69d9f3b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-25-21-27-37.gh-issue-86463.lcBmsO.rst @@ -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.