From f7d939b6614ca2f703d057690ef6b3db7fac16ee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 25 Sep 2024 21:28:21 +0300 Subject: [PATCH] gh-86463: Fix default prog in subparsers if usage is used in the 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. --- Doc/library/argparse.rst | 3 ++ Lib/argparse.py | 13 +++-- Lib/test/test_argparse.py | 51 +++++++++++++++++-- ...4-09-25-21-27-37.gh-issue-86463.lcBmsO.rst | 3 ++ 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-25-21-27-37.gh-issue-86463.lcBmsO.rst 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.