Skip to content

Commit cffb4c7

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

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
@@ -2004,7 +2004,7 @@ def consume_optional(start_index):
20042004

20052005
# get the optional identified at this index
20062006
option_tuple = option_string_indices[start_index]
2007-
action, option_string, explicit_arg = option_tuple
2007+
action, option_string, sep, explicit_arg = option_tuple
20082008

20092009
# identify additional optionals in the same arg string
20102010
# (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2031,18 +2031,27 @@ def consume_optional(start_index):
20312031
and option_string[1] not in chars
20322032
and explicit_arg != ''
20332033
):
2034+
if sep or explicit_arg[0] in chars:
2035+
msg = _('ignored explicit argument %r')
2036+
raise ArgumentError(action, msg % explicit_arg)
20342037
action_tuples.append((action, [], option_string))
20352038
char = option_string[0]
20362039
option_string = char + explicit_arg[0]
2037-
new_explicit_arg = explicit_arg[1:] or None
20382040
optionals_map = self._option_string_actions
20392041
if option_string in optionals_map:
20402042
action = optionals_map[option_string]
2041-
explicit_arg = new_explicit_arg
2043+
explicit_arg = explicit_arg[1:]
2044+
if not explicit_arg:
2045+
sep = explicit_arg = None
2046+
elif explicit_arg[0] == '=':
2047+
sep = '='
2048+
explicit_arg = explicit_arg[1:]
2049+
else:
2050+
sep = ''
20422051
else:
2043-
msg = _('ignored explicit argument %r')
2044-
raise ArgumentError(action, msg % explicit_arg)
2045-
2052+
extras.append(char + explicit_arg)
2053+
stop = start_index + 1
2054+
break
20462055
# if the action expect exactly one argument, we've
20472056
# successfully matched the option; exit the loop
20482057
elif arg_count == 1:
@@ -2262,18 +2271,17 @@ def _parse_optional(self, arg_string):
22622271
# if the option string is present in the parser, return the action
22632272
if arg_string in self._option_string_actions:
22642273
action = self._option_string_actions[arg_string]
2265-
return action, arg_string, None
2274+
return action, arg_string, None, None
22662275

22672276
# if it's just a single character, it was meant to be positional
22682277
if len(arg_string) == 1:
22692278
return None
22702279

22712280
# if the option string before the "=" is present, return the action
2272-
if '=' in arg_string:
2273-
option_string, explicit_arg = arg_string.split('=', 1)
2274-
if option_string in self._option_string_actions:
2275-
action = self._option_string_actions[option_string]
2276-
return action, option_string, explicit_arg
2281+
option_string, sep, explicit_arg = arg_string.partition('=')
2282+
if sep and option_string in self._option_string_actions:
2283+
action = self._option_string_actions[option_string]
2284+
return action, option_string, sep, explicit_arg
22772285

22782286
# search through all possible prefixes of the option string
22792287
# and all actions in the parser for possible interpretations
@@ -2282,7 +2290,7 @@ def _parse_optional(self, arg_string):
22822290
# if multiple actions match, the option string was ambiguous
22832291
if len(option_tuples) > 1:
22842292
options = ', '.join([option_string
2285-
for action, option_string, explicit_arg in option_tuples])
2293+
for action, option_string, sep, explicit_arg in option_tuples])
22862294
args = {'option': arg_string, 'matches': options}
22872295
msg = _('ambiguous option: %(option)s could match %(matches)s')
22882296
self.error(msg % args)
@@ -2306,7 +2314,7 @@ def _parse_optional(self, arg_string):
23062314

23072315
# it was meant to be an optional but there is no such option
23082316
# in this parser (though it might be a valid option in a subparser)
2309-
return None, arg_string, None
2317+
return None, arg_string, None, None
23102318

23112319
def _get_option_tuples(self, option_string):
23122320
result = []
@@ -2316,34 +2324,31 @@ def _get_option_tuples(self, option_string):
23162324
chars = self.prefix_chars
23172325
if option_string[0] in chars and option_string[1] in chars:
23182326
if self.allow_abbrev:
2319-
if '=' in option_string:
2320-
option_prefix, explicit_arg = option_string.split('=', 1)
2321-
else:
2322-
option_prefix = option_string
2323-
explicit_arg = None
2327+
option_prefix, sep, explicit_arg = option_string.partition('=')
2328+
if not sep:
2329+
sep = explicit_arg = None
23242330
for option_string in self._option_string_actions:
23252331
if option_string.startswith(option_prefix):
23262332
action = self._option_string_actions[option_string]
2327-
tup = action, option_string, explicit_arg
2333+
tup = action, option_string, sep, explicit_arg
23282334
result.append(tup)
23292335

23302336
# single character options can be concatenated with their arguments
23312337
# but multiple character options always have to have their argument
23322338
# separate
23332339
elif option_string[0] in chars and option_string[1] not in chars:
23342340
option_prefix = option_string
2335-
explicit_arg = None
23362341
short_option_prefix = option_string[:2]
23372342
short_explicit_arg = option_string[2:]
23382343

23392344
for option_string in self._option_string_actions:
23402345
if option_string == short_option_prefix:
23412346
action = self._option_string_actions[option_string]
2342-
tup = action, option_string, short_explicit_arg
2347+
tup = action, option_string, '', short_explicit_arg
23432348
result.append(tup)
23442349
elif option_string.startswith(option_prefix):
23452350
action = self._option_string_actions[option_string]
2346-
tup = action, option_string, explicit_arg
2351+
tup = action, option_string, None, None
23472352
result.append(tup)
23482353

23492354
# shouldn't ever get here

Lib/test/test_argparse.py

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

2213+
def test_parse_known_args_with_single_dash_option(self):
2214+
parser = ErrorRaisingArgumentParser()
2215+
parser.add_argument('-k', '--known', action='count', default=0)
2216+
parser.add_argument('-n', '--new', action='count', default=0)
2217+
self.assertEqual(parser.parse_known_args(['-k', '-u']),
2218+
(NS(known=1, new=0), ['-u']))
2219+
self.assertEqual(parser.parse_known_args(['-u', '-k']),
2220+
(NS(known=1, new=0), ['-u']))
2221+
self.assertEqual(parser.parse_known_args(['-ku']),
2222+
(NS(known=1, new=0), ['-u']))
2223+
self.assertArgumentParserError(parser.parse_known_args, ['-k=u'])
2224+
self.assertEqual(parser.parse_known_args(['-uk']),
2225+
(NS(known=0, new=0), ['-uk']))
2226+
self.assertEqual(parser.parse_known_args(['-u=k']),
2227+
(NS(known=0, new=0), ['-u=k']))
2228+
self.assertEqual(parser.parse_known_args(['-kunknown']),
2229+
(NS(known=1, new=0), ['-unknown']))
2230+
self.assertArgumentParserError(parser.parse_known_args, ['-k=unknown'])
2231+
self.assertEqual(parser.parse_known_args(['-ku=nknown']),
2232+
(NS(known=1, new=0), ['-u=nknown']))
2233+
self.assertEqual(parser.parse_known_args(['-knew']),
2234+
(NS(known=1, new=1), ['-ew']))
2235+
self.assertArgumentParserError(parser.parse_known_args, ['-kn=ew'])
2236+
self.assertArgumentParserError(parser.parse_known_args, ['-k-new'])
2237+
self.assertArgumentParserError(parser.parse_known_args, ['-kn-ew'])
2238+
self.assertEqual(parser.parse_known_args(['-kne-w']),
2239+
(NS(known=1, new=1), ['-e-w']))
2240+
22132241
def test_dest(self):
22142242
parser = ErrorRaisingArgumentParser()
22152243
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)