Skip to content

Commit c02b7ae

Browse files
[3.11] gh-60346: Improve handling single-dash options in ArgumentParser.parse_known_args() (GH-114180) (GH-115674)
(cherry picked from commit e47ecbd) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent e1994c4 commit c02b7ae

File tree

3 files changed

+57
-23
lines changed

3 files changed

+57
-23
lines changed

Lib/argparse.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,7 +1982,7 @@ def consume_optional(start_index):
19821982

19831983
# get the optional identified at this index
19841984
option_tuple = option_string_indices[start_index]
1985-
action, option_string, explicit_arg = option_tuple
1985+
action, option_string, sep, explicit_arg = option_tuple
19861986

19871987
# identify additional optionals in the same arg string
19881988
# (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2009,18 +2009,27 @@ def consume_optional(start_index):
20092009
and option_string[1] not in chars
20102010
and explicit_arg != ''
20112011
):
2012+
if sep or explicit_arg[0] in chars:
2013+
msg = _('ignored explicit argument %r')
2014+
raise ArgumentError(action, msg % explicit_arg)
20122015
action_tuples.append((action, [], option_string))
20132016
char = option_string[0]
20142017
option_string = char + explicit_arg[0]
2015-
new_explicit_arg = explicit_arg[1:] or None
20162018
optionals_map = self._option_string_actions
20172019
if option_string in optionals_map:
20182020
action = optionals_map[option_string]
2019-
explicit_arg = new_explicit_arg
2021+
explicit_arg = explicit_arg[1:]
2022+
if not explicit_arg:
2023+
sep = explicit_arg = None
2024+
elif explicit_arg[0] == '=':
2025+
sep = '='
2026+
explicit_arg = explicit_arg[1:]
2027+
else:
2028+
sep = ''
20202029
else:
2021-
msg = _('ignored explicit argument %r')
2022-
raise ArgumentError(action, msg % explicit_arg)
2023-
2030+
extras.append(char + explicit_arg)
2031+
stop = start_index + 1
2032+
break
20242033
# if the action expect exactly one argument, we've
20252034
# successfully matched the option; exit the loop
20262035
elif arg_count == 1:
@@ -2238,18 +2247,17 @@ def _parse_optional(self, arg_string):
22382247
# if the option string is present in the parser, return the action
22392248
if arg_string in self._option_string_actions:
22402249
action = self._option_string_actions[arg_string]
2241-
return action, arg_string, None
2250+
return action, arg_string, None, None
22422251

22432252
# if it's just a single character, it was meant to be positional
22442253
if len(arg_string) == 1:
22452254
return None
22462255

22472256
# if the option string before the "=" is present, return the action
2248-
if '=' in arg_string:
2249-
option_string, explicit_arg = arg_string.split('=', 1)
2250-
if option_string in self._option_string_actions:
2251-
action = self._option_string_actions[option_string]
2252-
return action, option_string, explicit_arg
2257+
option_string, sep, explicit_arg = arg_string.partition('=')
2258+
if sep and option_string in self._option_string_actions:
2259+
action = self._option_string_actions[option_string]
2260+
return action, option_string, sep, explicit_arg
22532261

22542262
# search through all possible prefixes of the option string
22552263
# and all actions in the parser for possible interpretations
@@ -2258,7 +2266,7 @@ def _parse_optional(self, arg_string):
22582266
# if multiple actions match, the option string was ambiguous
22592267
if len(option_tuples) > 1:
22602268
options = ', '.join([option_string
2261-
for action, option_string, explicit_arg in option_tuples])
2269+
for action, option_string, sep, explicit_arg in option_tuples])
22622270
args = {'option': arg_string, 'matches': options}
22632271
msg = _('ambiguous option: %(option)s could match %(matches)s')
22642272
self.error(msg % args)
@@ -2282,7 +2290,7 @@ def _parse_optional(self, arg_string):
22822290

22832291
# it was meant to be an optional but there is no such option
22842292
# in this parser (though it might be a valid option in a subparser)
2285-
return None, arg_string, None
2293+
return None, arg_string, None, None
22862294

22872295
def _get_option_tuples(self, option_string):
22882296
result = []
@@ -2292,34 +2300,31 @@ def _get_option_tuples(self, option_string):
22922300
chars = self.prefix_chars
22932301
if option_string[0] in chars and option_string[1] in chars:
22942302
if self.allow_abbrev:
2295-
if '=' in option_string:
2296-
option_prefix, explicit_arg = option_string.split('=', 1)
2297-
else:
2298-
option_prefix = option_string
2299-
explicit_arg = None
2303+
option_prefix, sep, explicit_arg = option_string.partition('=')
2304+
if not sep:
2305+
sep = explicit_arg = None
23002306
for option_string in self._option_string_actions:
23012307
if option_string.startswith(option_prefix):
23022308
action = self._option_string_actions[option_string]
2303-
tup = action, option_string, explicit_arg
2309+
tup = action, option_string, sep, explicit_arg
23042310
result.append(tup)
23052311

23062312
# single character options can be concatenated with their arguments
23072313
# but multiple character options always have to have their argument
23082314
# separate
23092315
elif option_string[0] in chars and option_string[1] not in chars:
23102316
option_prefix = option_string
2311-
explicit_arg = None
23122317
short_option_prefix = option_string[:2]
23132318
short_explicit_arg = option_string[2:]
23142319

23152320
for option_string in self._option_string_actions:
23162321
if option_string == short_option_prefix:
23172322
action = self._option_string_actions[option_string]
2318-
tup = action, option_string, short_explicit_arg
2323+
tup = action, option_string, '', short_explicit_arg
23192324
result.append(tup)
23202325
elif option_string.startswith(option_prefix):
23212326
action = self._option_string_actions[option_string]
2322-
tup = action, option_string, explicit_arg
2327+
tup = action, option_string, None, None
23232328
result.append(tup)
23242329

23252330
# shouldn't ever get here

Lib/test/test_argparse.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,6 +2156,34 @@ def test_parse_known_args(self):
21562156
(NS(foo=False, bar=0.5, w=7, x='b'), ['-W', '-X', 'Y', 'Z']),
21572157
)
21582158

2159+
def test_parse_known_args_with_single_dash_option(self):
2160+
parser = ErrorRaisingArgumentParser()
2161+
parser.add_argument('-k', '--known', action='count', default=0)
2162+
parser.add_argument('-n', '--new', action='count', default=0)
2163+
self.assertEqual(parser.parse_known_args(['-k', '-u']),
2164+
(NS(known=1, new=0), ['-u']))
2165+
self.assertEqual(parser.parse_known_args(['-u', '-k']),
2166+
(NS(known=1, new=0), ['-u']))
2167+
self.assertEqual(parser.parse_known_args(['-ku']),
2168+
(NS(known=1, new=0), ['-u']))
2169+
self.assertArgumentParserError(parser.parse_known_args, ['-k=u'])
2170+
self.assertEqual(parser.parse_known_args(['-uk']),
2171+
(NS(known=0, new=0), ['-uk']))
2172+
self.assertEqual(parser.parse_known_args(['-u=k']),
2173+
(NS(known=0, new=0), ['-u=k']))
2174+
self.assertEqual(parser.parse_known_args(['-kunknown']),
2175+
(NS(known=1, new=0), ['-unknown']))
2176+
self.assertArgumentParserError(parser.parse_known_args, ['-k=unknown'])
2177+
self.assertEqual(parser.parse_known_args(['-ku=nknown']),
2178+
(NS(known=1, new=0), ['-u=nknown']))
2179+
self.assertEqual(parser.parse_known_args(['-knew']),
2180+
(NS(known=1, new=1), ['-ew']))
2181+
self.assertArgumentParserError(parser.parse_known_args, ['-kn=ew'])
2182+
self.assertArgumentParserError(parser.parse_known_args, ['-k-new'])
2183+
self.assertArgumentParserError(parser.parse_known_args, ['-kn-ew'])
2184+
self.assertEqual(parser.parse_known_args(['-kne-w']),
2185+
(NS(known=1, new=1), ['-e-w']))
2186+
21592187
def test_dest(self):
21602188
parser = ErrorRaisingArgumentParser()
21612189
parser.add_argument('--foo', action='store_true')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ArgumentParser inconsistent with parse_known_args.

0 commit comments

Comments
 (0)