From 3c5f6a5b8ee6b802f3ff5439829cf2b612382ab4 Mon Sep 17 00:00:00 2001 From: furkanonder Date: Sun, 2 Apr 2023 23:42:58 +0300 Subject: [PATCH 1/3] Fix ArgumentParser inconsistent with parse_known_args --- Lib/argparse.py | 5 ++--- Lib/test/test_argparse.py | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index a819d2650e85f0..cde7cc5009251c 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2018,9 +2018,8 @@ def consume_optional(start_index): action = optionals_map[option_string] explicit_arg = new_explicit_arg else: - msg = _('ignored explicit argument %r') - raise ArgumentError(action, msg % explicit_arg) - + extras.append(option_string) + explicit_arg = new_explicit_arg # if the action expect exactly one argument, we've # successfully matched the option; exit the loop elif arg_count == 1: diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 861da2326d1214..2856be3d03418f 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -2136,6 +2136,13 @@ def test_parse_known_args(self): (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 = argparse.ArgumentParser() + parser.add_argument('-k', '--known', action='store_true') + self.assertEqual(parser.parse_known_args(['-k', '-u']), (NS(known=True), ['-u'])) + self.assertEqual(parser.parse_known_args(['-ku']), (NS(known=True), ['-u'])) + self.assertEqual(parser.parse_known_args(['-uk']), (NS(known=False), ['-uk'])) + def test_dest(self): parser = ErrorRaisingArgumentParser() parser.add_argument('--foo', action='store_true') From 04d7adc0ac217f4331feeb1d3fed5aea5c27fab0 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 2 Apr 2023 21:20:39 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst 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 index 00000000000000..c15bd6ed11d17f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst @@ -0,0 +1 @@ +Fix ArgumentParser inconsistent with parse_known_args. From fadc5fd38ac8d5538aeaf1236cfe2614a376647f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 17 Jan 2024 13:08:58 +0200 Subject: [PATCH 3/3] Handle more corner cases. --- Lib/argparse.py | 50 ++++++++++++++++++++++----------------- Lib/test/test_argparse.py | 31 ++++++++++++++++++++---- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 6f0522d8cc7b1e..c7c5f41fa48f66 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2005,7 +2005,7 @@ def consume_optional(start_index): # 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) @@ -2032,17 +2032,27 @@ def consume_optional(start_index): 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: - extras.append(option_string) - explicit_arg = new_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 +2272,17 @@ def _parse_optional(self, arg_string): # 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 +2291,7 @@ def _parse_optional(self, arg_string): # 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 +2315,7 @@ def _parse_optional(self, arg_string): # 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 +2325,13 @@ def _get_option_tuples(self, option_string): 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 +2339,17 @@ def _get_option_tuples(self, option_string): # 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 diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 421bc9138a76d3..3221d564d198ed 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -2275,11 +2275,32 @@ def test_parse_known_args(self): ) def test_parse_known_args_with_single_dash_option(self): - parser = argparse.ArgumentParser() - parser.add_argument('-k', '--known', action='store_true') - self.assertEqual(parser.parse_known_args(['-k', '-u']), (NS(known=True), ['-u'])) - self.assertEqual(parser.parse_known_args(['-ku']), (NS(known=True), ['-u'])) - self.assertEqual(parser.parse_known_args(['-uk']), (NS(known=False), ['-uk'])) + 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()