diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 995c4ee28c7..d285c662218 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1668,6 +1668,18 @@ Sub-commands >>> parser.parse_args(['co', 'bar']) Namespace(foo='bar') + argparse supports non-ambiguous abbreviations of subparser names. + + For example, the following three calls are all supported + and do the same thing, as the abbreviations used are not ambiguous:: + + >>> parser.parse_args(['checkout', 'bar']) + Namespace(foo='bar') + >>> parser.parse_args(['check', 'bar']) + Namespace(foo='bar') + >>> parser.parse_args(['che', 'bar']) + Namespace(foo='bar') + One particularly effective way of handling sub-commands is to combine the use of the :meth:`add_subparsers` method with calls to :meth:`set_defaults` so that each subparser knows which Python function it should execute. For diff --git a/Lib/argparse.py b/Lib/argparse.py index 209b4e99c1f..e0edad8e420 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1110,6 +1110,12 @@ class _SubParsersAction(Action): parser_name = values[0] arg_strings = values[1:] + # get full parser_name from (optional) abbreviated one + for p in self._name_parser_map: + if p.startswith(parser_name): + parser_name = p + break + # set the parser name if requested if self.dest is not SUPPRESS: setattr(namespace, self.dest, parser_name) @@ -2307,11 +2313,18 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): def _check_value(self, action, value): # converted value must be one of the choices (if specified) - if action.choices is not None and value not in action.choices: - args = {'value': value, - 'choices': ', '.join(map(repr, action.choices))} - msg = _('invalid choice: %(value)r (choose from %(choices)s)') - raise ArgumentError(action, msg % args) + if action.choices is not None: + ac = [ax for ax in action.choices if str(ax).startswith(str(value))] + if len(ac) == 0: + args = {'value': value, + 'choices': ', '.join(map(repr, action.choices))} + msg = _('invalid choice: %(value)r (choose from %(choices)s)') + raise ArgumentError(action, msg % args) + elif len(ac) > 1: + args = {'value': value, + 'choices': ', '.join(ac)} + msg = _('ambiguous choice: %(value)r could match %(choices)s') + raise ArgumentError(action, msg % args) # ======================= # Help-formatting methods diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 52c624771ce..32d1b0cbb9e 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1842,6 +1842,22 @@ class TestAddSubparsers(TestCase): parser3.add_argument('t', type=int, help='t help') parser3.add_argument('u', nargs='...', help='u help') + # add fourth sub-parser (to test abbreviations) + parser4_kwargs = dict(description='lost description') + if subparser_help: + parser4_kwargs['help'] = 'lost help' + parser4 = subparsers.add_parser('lost', **parser4_kwargs) + parser4.add_argument('-w', type=int, help='w help') + parser4.add_argument('x', choices='abc', help='x help') + + # add fifth sub-parser, with longer name (to test abbreviations) + parser5_kwargs = dict(description='long description') + if subparser_help: + parser5_kwargs['help'] = 'long help' + parser5 = subparsers.add_parser('long', **parser5_kwargs) + parser5.add_argument('-w', type=int, help='w help') + parser5.add_argument('x', choices='abc', help='x help') + # return the main parser return parser @@ -1857,6 +1873,24 @@ class TestAddSubparsers(TestCase): args = args_str.split() self.assertArgumentParserError(self.parser.parse_args, args) + def test_parse_args_abbreviation(self): + # check some non-failure cases: + self.assertEqual( + self.parser.parse_args('0.5 long b -w 7'.split()), + NS(foo=False, bar=0.5, w=7, x='b'), + ) + self.assertEqual( + self.parser.parse_args('0.5 lon b -w 7'.split()), + NS(foo=False, bar=0.5, w=7, x='b'), + ) + self.assertEqual( + self.parser.parse_args('0.5 los b -w 7'.split()), + NS(foo=False, bar=0.5, w=7, x='b'), + ) + # check a failure case: 'lo' is ambiguous + self.assertArgumentParserError(self.parser.parse_args, + '0.5 lo b -w 7'.split()) + def test_parse_args(self): # check some non-failure cases: self.assertEqual( @@ -1909,78 +1943,80 @@ class TestAddSubparsers(TestCase): def test_help(self): self.assertEqual(self.parser.format_usage(), - 'usage: PROG [-h] [--foo] bar {1,2,3} ...\n') + 'usage: PROG [-h] [--foo] bar {1,2,3,lost,long} ...\n') self.assertEqual(self.parser.format_help(), textwrap.dedent('''\ - usage: PROG [-h] [--foo] bar {1,2,3} ... + usage: PROG [-h] [--foo] bar {1,2,3,lost,long} ... main description positional arguments: - bar bar help - {1,2,3} command help + bar bar help + {1,2,3,lost,long} command help optional arguments: - -h, --help show this help message and exit - --foo foo help + -h, --help show this help message and exit + --foo foo help ''')) def test_help_extra_prefix_chars(self): # Make sure - is still used for help if it is a non-first prefix char parser = self._get_parser(prefix_chars='+:-') self.assertEqual(parser.format_usage(), - 'usage: PROG [-h] [++foo] bar {1,2,3} ...\n') + 'usage: PROG [-h] [++foo] bar {1,2,3,lost,long} ...\n') self.assertEqual(parser.format_help(), textwrap.dedent('''\ - usage: PROG [-h] [++foo] bar {1,2,3} ... + usage: PROG [-h] [++foo] bar {1,2,3,lost,long} ... main description positional arguments: - bar bar help - {1,2,3} command help + bar bar help + {1,2,3,lost,long} command help optional arguments: - -h, --help show this help message and exit - ++foo foo help + -h, --help show this help message and exit + ++foo foo help ''')) def test_help_alternate_prefix_chars(self): parser = self._get_parser(prefix_chars='+:/') self.assertEqual(parser.format_usage(), - 'usage: PROG [+h] [++foo] bar {1,2,3} ...\n') + 'usage: PROG [+h] [++foo] bar {1,2,3,lost,long} ...\n') self.assertEqual(parser.format_help(), textwrap.dedent('''\ - usage: PROG [+h] [++foo] bar {1,2,3} ... + usage: PROG [+h] [++foo] bar {1,2,3,lost,long} ... main description positional arguments: - bar bar help - {1,2,3} command help + bar bar help + {1,2,3,lost,long} command help optional arguments: - +h, ++help show this help message and exit - ++foo foo help + +h, ++help show this help message and exit + ++foo foo help ''')) def test_parser_command_help(self): self.assertEqual(self.command_help_parser.format_usage(), - 'usage: PROG [-h] [--foo] bar {1,2,3} ...\n') + 'usage: PROG [-h] [--foo] bar {1,2,3,lost,long} ...\n') self.assertEqual(self.command_help_parser.format_help(), textwrap.dedent('''\ - usage: PROG [-h] [--foo] bar {1,2,3} ... + usage: PROG [-h] [--foo] bar {1,2,3,lost,long} ... main description positional arguments: - bar bar help - {1,2,3} command help - 1 1 help - 2 2 help - 3 3 help + bar bar help + {1,2,3,lost,long} command help + 1 1 help + 2 2 help + 3 3 help + lost lost help + long long help optional arguments: - -h, --help show this help message and exit - --foo foo help + -h, --help show this help message and exit + --foo foo help ''')) def test_subparser_title_help(self): @@ -2083,6 +2119,8 @@ class TestAddSubparsers(TestCase): 1 help 2 2 help 3 3 help + lost lost help + long long help """)) # ============