diff -r 9ea84f006892 Lib/argparse.py --- a/Lib/argparse.py Wed May 01 15:15:50 2013 +0200 +++ b/Lib/argparse.py Sat Sep 28 21:22:30 2013 -0700 @@ -2064,6 +2064,20 @@ # return the list of arg string counts return result + def _get_nested_action(self, arg_string): + # recursively seek arg_string in subparsers + if self._subparsers is not None: + for action in self._subparsers._actions: + if isinstance(action, _SubParsersAction): + for parser in action._name_parser_map.values(): + if arg_string in parser._option_string_actions: + return parser._option_string_actions[arg_string] + else: + sub_action = parser._get_nested_action(arg_string) + if sub_action is not None: + return sub_action + return None + def _parse_optional(self, arg_string): # if it's an empty string, it was meant to be a positional if not arg_string: @@ -2078,6 +2092,13 @@ action = self._option_string_actions[arg_string] return action, arg_string, None + if getattr(self, 'scan', True): + # parser.scan temporary testing switch + # if arg_string is found in a subparser, treat as an unknown + # optional + if self._get_nested_action(arg_string): + return None, arg_string, None + # if it's just a single character, it was meant to be positional if len(arg_string) == 1: return None diff -r 9ea84f006892 Lib/test/test_argparse.py --- a/Lib/test/test_argparse.py Wed May 01 15:15:50 2013 +0200 +++ b/Lib/test/test_argparse.py Sat Sep 28 21:22:30 2013 -0700 @@ -2066,6 +2066,124 @@ 3 3 help """)) +#=============== +# Subparsers ambiguity tests +#=============== + +class TestSubparserOptionals(TestCase): + """Test subparser arguments that may be ambiguous in base parser + argparse would issue + 'error: ambiguous option: --foo could match --foo1, --foo2' + even though --foo is an argument of subparser + subparsers used to be required; but a change in required testing + changed that. + Test both ways with explicited 'required' attribute setting + """ + def assertArgumentParserError(self, *args, **kwargs): + self.assertRaises(ArgumentParserError, *args, **kwargs) + + def setUp(self): + parser = ErrorRaisingArgumentParser() + parser.scan = True # testing switch + parser.add_argument('--foo1','--bar1', action='store_true') + parser.add_argument('--foo2','--bar2', action='store_true') + subparsers = parser.add_subparsers(dest='cmd') + cmd1 = subparsers.add_parser('cmd1') + cmd1.add_argument('--foo', action='store_true') + cmd2 = subparsers.add_parser('cmd2') + # nested subparser + sub2 = cmd2.add_subparsers(dest='sub') + sub2.required = True + cmd21 = sub2.add_parser('cmd21') + cmd21.add_argument('--bar', action='store_true') + self.parser = parser + self.subparsers = subparsers + + def test_parse_args_failures_required(self): + self.subparsers.required = True + for args_str in [ + '', + '--foo1', + '--foo', + '--foo cmd1', + ]: + args = args_str.split() + self.assertArgumentParserError(self.parser.parse_args, args) + + def test_parse_args_errormsg(self): + """test error message; issue arose because argparse used issue + rror: ambiguous option: --foo could match --foo1, --foo2""" + for args_str, regex in [ + ['--foo', '[required|unrecognized]'], + ['--foo cmd1', 'unrecognized'], + ]: + args = args_str.split() + with self.assertRaises(ArgumentParserError) as cm: + self.parser.parse_args(args) + msg = str(cm.exception) + self.assertRegex(msg, regex) + self.assertNotRegex(msg, 'ambiguous option') + + def test_parse_args_required(self): + self.subparsers.required = True + self.assertEqual( + self.parser.parse_args('cmd1 --foo'.split()), + NS(cmd='cmd1', foo=True, foo1=False, foo2=False), + ) + self.assertEqual( + self.parser.parse_args('--foo1 cmd1 --foo'.split()), + NS(cmd='cmd1', foo=True, foo1=True, foo2=False), + ) + + def test_parse_args_failures_notrequired(self): + self.subparsers.required = False + for args_str in [ + '--foo', + '--foo cmd1', + ]: + args = args_str.split() + self.assertArgumentParserError(self.parser.parse_args, args) + + def test_parse_args_notrequired(self): + self.subparsers.required = False + self.assertEqual( + self.parser.parse_args('cmd1 --foo'.split()), + NS(cmd='cmd1', foo=True, foo1=False, foo2=False), + ) + self.assertEqual( + self.parser.parse_args('--foo1 cmd1 --foo'.split()), + NS(cmd='cmd1', foo=True, foo1=True, foo2=False), + ) + + def test_parse_args_failures_sub(self): + "test that involves 2 levels of subparsers" + self.subparsers.required = True + for args_str in [ + '', + '--bar1', + '--bar', + '--bar cmd2', + 'cmd2 --bar cmd21', + '--bar1 cmd2 --bar', + ]: + args = args_str.split() + self.assertArgumentParserError(self.parser.parse_args, args) + + def test_parse_args_sub(self): + """ensure that testing is recursive + without fix gives error + 'ambiguous option: --bar could match --bar1, --bar2' + """ + self.subparsers.required = True + self.assertEqual( + self.parser.parse_args('cmd2 cmd21 --bar'.split()), + NS(cmd='cmd2', bar=True, foo1=False, foo2=False, sub='cmd21'), + ) + self.assertEqual( + self.parser.parse_args('--bar1 cmd2 cmd21 --bar'.split()), + NS(cmd='cmd2', bar=True, foo1=True, foo2=False, sub='cmd21'), + ) + # ============ # Groups tests # ============