]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.12] gh-60346: Improve handling single-dash options in ArgumentParser.parse_known_a...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 19 Feb 2024 18:25:09 +0000 (19:25 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Feb 2024 18:25:09 +0000 (18:25 +0000)
(cherry picked from commit e47ecbd0420528f1f9f282d9e7acfcf586a4caa1)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Lib/argparse.py
Lib/test/test_argparse.py
Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst [new file with mode: 0644]

index 484a1efde43cf1985e8bd69b7e7c8950a033019d..462cf2cdc7f6bbaa2c597926a2a48018e4ac8bf5 100644 (file)
@@ -2004,7 +2004,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
 
             # get the optional identified at this index
             option_tuple = option_string_indices[start_index]
-            action, option_string, explicit_arg = option_tuple
+            action, option_string, sep, explicit_arg = option_tuple
 
             # identify additional optionals in the same arg string
             # (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2031,18 +2031,27 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
                         and option_string[1] not in chars
                         and explicit_arg != ''
                     ):
+                        if sep or explicit_arg[0] in chars:
+                            msg = _('ignored explicit argument %r')
+                            raise ArgumentError(action, msg % explicit_arg)
                         action_tuples.append((action, [], option_string))
                         char = option_string[0]
                         option_string = char + explicit_arg[0]
-                        new_explicit_arg = explicit_arg[1:] or None
                         optionals_map = self._option_string_actions
                         if option_string in optionals_map:
                             action = optionals_map[option_string]
-                            explicit_arg = new_explicit_arg
+                            explicit_arg = explicit_arg[1:]
+                            if not explicit_arg:
+                                sep = explicit_arg = None
+                            elif explicit_arg[0] == '=':
+                                sep = '='
+                                explicit_arg = explicit_arg[1:]
+                            else:
+                                sep = ''
                         else:
-                            msg = _('ignored explicit argument %r')
-                            raise ArgumentError(action, msg % explicit_arg)
-
+                            extras.append(char + explicit_arg)
+                            stop = start_index + 1
+                            break
                     # if the action expect exactly one argument, we've
                     # successfully matched the option; exit the loop
                     elif arg_count == 1:
@@ -2262,18 +2271,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
         # if the option string is present in the parser, return the action
         if arg_string in self._option_string_actions:
             action = self._option_string_actions[arg_string]
-            return action, arg_string, None
+            return action, arg_string, None, None
 
         # if it's just a single character, it was meant to be positional
         if len(arg_string) == 1:
             return None
 
         # if the option string before the "=" is present, return the action
-        if '=' in arg_string:
-            option_string, explicit_arg = arg_string.split('=', 1)
-            if option_string in self._option_string_actions:
-                action = self._option_string_actions[option_string]
-                return action, option_string, explicit_arg
+        option_string, sep, explicit_arg = arg_string.partition('=')
+        if sep and option_string in self._option_string_actions:
+            action = self._option_string_actions[option_string]
+            return action, option_string, sep, explicit_arg
 
         # search through all possible prefixes of the option string
         # and all actions in the parser for possible interpretations
@@ -2282,7 +2290,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
         # if multiple actions match, the option string was ambiguous
         if len(option_tuples) > 1:
             options = ', '.join([option_string
-                for action, option_string, explicit_arg in option_tuples])
+                for action, option_string, sep, explicit_arg in option_tuples])
             args = {'option': arg_string, 'matches': options}
             msg = _('ambiguous option: %(option)s could match %(matches)s')
             self.error(msg % args)
@@ -2306,7 +2314,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
 
         # it was meant to be an optional but there is no such option
         # in this parser (though it might be a valid option in a subparser)
-        return None, arg_string, None
+        return None, arg_string, None, None
 
     def _get_option_tuples(self, option_string):
         result = []
@@ -2316,15 +2324,13 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
         chars = self.prefix_chars
         if option_string[0] in chars and option_string[1] in chars:
             if self.allow_abbrev:
-                if '=' in option_string:
-                    option_prefix, explicit_arg = option_string.split('=', 1)
-                else:
-                    option_prefix = option_string
-                    explicit_arg = None
+                option_prefix, sep, explicit_arg = option_string.partition('=')
+                if not sep:
+                    sep = explicit_arg = None
                 for option_string in self._option_string_actions:
                     if option_string.startswith(option_prefix):
                         action = self._option_string_actions[option_string]
-                        tup = action, option_string, explicit_arg
+                        tup = action, option_string, sep, explicit_arg
                         result.append(tup)
 
         # single character options can be concatenated with their arguments
@@ -2332,18 +2338,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
         # separate
         elif option_string[0] in chars and option_string[1] not in chars:
             option_prefix = option_string
-            explicit_arg = None
             short_option_prefix = option_string[:2]
             short_explicit_arg = option_string[2:]
 
             for option_string in self._option_string_actions:
                 if option_string == short_option_prefix:
                     action = self._option_string_actions[option_string]
-                    tup = action, option_string, short_explicit_arg
+                    tup = action, option_string, '', short_explicit_arg
                     result.append(tup)
                 elif option_string.startswith(option_prefix):
                     action = self._option_string_actions[option_string]
-                    tup = action, option_string, explicit_arg
+                    tup = action, option_string, None, None
                     result.append(tup)
 
         # shouldn't ever get here
index 88cc62a4cd39ff7771ff7be8e1f3b698a8d167f9..5a953088e66518d99a00c4bc8eb94bc7ab031059 100644 (file)
@@ -2210,6 +2210,34 @@ class TestAddSubparsers(TestCase):
             (NS(foo=False, bar=0.5, w=7, x='b'), ['-W', '-X', 'Y', 'Z']),
         )
 
+    def test_parse_known_args_with_single_dash_option(self):
+        parser = ErrorRaisingArgumentParser()
+        parser.add_argument('-k', '--known', action='count', default=0)
+        parser.add_argument('-n', '--new', action='count', default=0)
+        self.assertEqual(parser.parse_known_args(['-k', '-u']),
+                         (NS(known=1, new=0), ['-u']))
+        self.assertEqual(parser.parse_known_args(['-u', '-k']),
+                         (NS(known=1, new=0), ['-u']))
+        self.assertEqual(parser.parse_known_args(['-ku']),
+                         (NS(known=1, new=0), ['-u']))
+        self.assertArgumentParserError(parser.parse_known_args, ['-k=u'])
+        self.assertEqual(parser.parse_known_args(['-uk']),
+                         (NS(known=0, new=0), ['-uk']))
+        self.assertEqual(parser.parse_known_args(['-u=k']),
+                         (NS(known=0, new=0), ['-u=k']))
+        self.assertEqual(parser.parse_known_args(['-kunknown']),
+                         (NS(known=1, new=0), ['-unknown']))
+        self.assertArgumentParserError(parser.parse_known_args, ['-k=unknown'])
+        self.assertEqual(parser.parse_known_args(['-ku=nknown']),
+                         (NS(known=1, new=0), ['-u=nknown']))
+        self.assertEqual(parser.parse_known_args(['-knew']),
+                         (NS(known=1, new=1), ['-ew']))
+        self.assertArgumentParserError(parser.parse_known_args, ['-kn=ew'])
+        self.assertArgumentParserError(parser.parse_known_args, ['-k-new'])
+        self.assertArgumentParserError(parser.parse_known_args, ['-kn-ew'])
+        self.assertEqual(parser.parse_known_args(['-kne-w']),
+                         (NS(known=1, new=1), ['-e-w']))
+
     def test_dest(self):
         parser = ErrorRaisingArgumentParser()
         parser.add_argument('--foo', action='store_true')
diff --git a/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst b/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst
new file mode 100644 (file)
index 0000000..c15bd6e
--- /dev/null
@@ -0,0 +1 @@
+Fix ArgumentParser inconsistent with parse_known_args.