Skip to content

Commit cbea45a

Browse files
[3.12] gh-58573: Fix conflicts between abbreviated long options in the parent parser and subparsers in argparse (GH-124631) (GH-124759)
Check for ambiguous options if the option is consumed, not when it is parsed. (cherry picked from commit 3f27153) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 5464c8a commit cbea45a

File tree

3 files changed

+52
-34
lines changed

3 files changed

+52
-34
lines changed

Lib/argparse.py

+28-34
Original file line numberDiff line numberDiff line change
@@ -1971,11 +1971,11 @@ def _parse_known_args(self, arg_strings, namespace):
19711971
# otherwise, add the arg to the arg strings
19721972
# and note the index if it was an option
19731973
else:
1974-
option_tuple = self._parse_optional(arg_string)
1975-
if option_tuple is None:
1974+
option_tuples = self._parse_optional(arg_string)
1975+
if option_tuples is None:
19761976
pattern = 'A'
19771977
else:
1978-
option_string_indices[i] = option_tuple
1978+
option_string_indices[i] = option_tuples
19791979
pattern = 'O'
19801980
arg_string_pattern_parts.append(pattern)
19811981

@@ -2009,8 +2009,16 @@ def take_action(action, argument_strings, option_string=None):
20092009
def consume_optional(start_index):
20102010

20112011
# get the optional identified at this index
2012-
option_tuple = option_string_indices[start_index]
2013-
action, option_string, sep, explicit_arg = option_tuple
2012+
option_tuples = option_string_indices[start_index]
2013+
# if multiple actions match, the option string was ambiguous
2014+
if len(option_tuples) > 1:
2015+
options = ', '.join([option_string
2016+
for action, option_string, sep, explicit_arg in option_tuples])
2017+
args = {'option': arg_string, 'matches': options}
2018+
msg = _('ambiguous option: %(option)s could match %(matches)s')
2019+
raise ArgumentError(None, msg % args)
2020+
2021+
action, option_string, sep, explicit_arg = option_tuples[0]
20142022

20152023
# identify additional optionals in the same arg string
20162024
# (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2287,7 +2295,7 @@ def _parse_optional(self, arg_string):
22872295
# if the option string is present in the parser, return the action
22882296
if arg_string in self._option_string_actions:
22892297
action = self._option_string_actions[arg_string]
2290-
return action, arg_string, None, None
2298+
return [(action, arg_string, None, None)]
22912299

22922300
# if it's just a single character, it was meant to be positional
22932301
if len(arg_string) == 1:
@@ -2297,25 +2305,14 @@ def _parse_optional(self, arg_string):
22972305
option_string, sep, explicit_arg = arg_string.partition('=')
22982306
if sep and option_string in self._option_string_actions:
22992307
action = self._option_string_actions[option_string]
2300-
return action, option_string, sep, explicit_arg
2308+
return [(action, option_string, sep, explicit_arg)]
23012309

23022310
# search through all possible prefixes of the option string
23032311
# and all actions in the parser for possible interpretations
23042312
option_tuples = self._get_option_tuples(arg_string)
23052313

2306-
# if multiple actions match, the option string was ambiguous
2307-
if len(option_tuples) > 1:
2308-
options = ', '.join([option_string
2309-
for action, option_string, sep, explicit_arg in option_tuples])
2310-
args = {'option': arg_string, 'matches': options}
2311-
msg = _('ambiguous option: %(option)s could match %(matches)s')
2312-
raise ArgumentError(None, msg % args)
2313-
2314-
# if exactly one action matched, this segmentation is good,
2315-
# so return the parsed action
2316-
elif len(option_tuples) == 1:
2317-
option_tuple, = option_tuples
2318-
return option_tuple
2314+
if option_tuples:
2315+
return option_tuples
23192316

23202317
# if it was not found as an option, but it looks like a negative
23212318
# number, it was meant to be positional
@@ -2330,7 +2327,7 @@ def _parse_optional(self, arg_string):
23302327

23312328
# it was meant to be an optional but there is no such option
23322329
# in this parser (though it might be a valid option in a subparser)
2333-
return None, arg_string, None, None
2330+
return [(None, arg_string, None, None)]
23342331

23352332
def _get_option_tuples(self, option_string):
23362333
result = []
@@ -2380,43 +2377,40 @@ def _get_nargs_pattern(self, action):
23802377
# in all examples below, we have to allow for '--' args
23812378
# which are represented as '-' in the pattern
23822379
nargs = action.nargs
2380+
# if this is an optional action, -- is not allowed
2381+
option = action.option_strings
23832382

23842383
# the default (None) is assumed to be a single argument
23852384
if nargs is None:
2386-
nargs_pattern = '(-*A-*)'
2385+
nargs_pattern = '([A])' if option else '(-*A-*)'
23872386

23882387
# allow zero or one arguments
23892388
elif nargs == OPTIONAL:
2390-
nargs_pattern = '(-*A?-*)'
2389+
nargs_pattern = '(A?)' if option else '(-*A?-*)'
23912390

23922391
# allow zero or more arguments
23932392
elif nargs == ZERO_OR_MORE:
2394-
nargs_pattern = '(-*[A-]*)'
2393+
nargs_pattern = '(A*)' if option else '(-*[A-]*)'
23952394

23962395
# allow one or more arguments
23972396
elif nargs == ONE_OR_MORE:
2398-
nargs_pattern = '(-*A[A-]*)'
2397+
nargs_pattern = '(A+)' if option else '(-*A[A-]*)'
23992398

24002399
# allow any number of options or arguments
24012400
elif nargs == REMAINDER:
2402-
nargs_pattern = '([-AO]*)'
2401+
nargs_pattern = '([AO]*)' if option else '(.*)'
24032402

24042403
# allow one argument followed by any number of options or arguments
24052404
elif nargs == PARSER:
2406-
nargs_pattern = '(-*A[-AO]*)'
2405+
nargs_pattern = '(A[AO]*)' if option else '(-*A[-AO]*)'
24072406

24082407
# suppress action, like nargs=0
24092408
elif nargs == SUPPRESS:
2410-
nargs_pattern = '(-*-*)'
2409+
nargs_pattern = '()' if option else '(-*)'
24112410

24122411
# all others should be integers
24132412
else:
2414-
nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
2415-
2416-
# if this is an optional action, -- is not allowed
2417-
if action.option_strings:
2418-
nargs_pattern = nargs_pattern.replace('-*', '')
2419-
nargs_pattern = nargs_pattern.replace('-', '')
2413+
nargs_pattern = '([AO]{%d})' % nargs if option else '((?:-*A){%d}-*)' % nargs
24202414

24212415
# return the pattern
24222416
return nargs_pattern

Lib/test/test_argparse.py

+22
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,28 @@ class C:
23132313
self.assertEqual(C.w, 7)
23142314
self.assertEqual(C.x, 'b')
23152315

2316+
def test_abbreviation(self):
2317+
parser = ErrorRaisingArgumentParser()
2318+
parser.add_argument('--foodle')
2319+
parser.add_argument('--foonly')
2320+
subparsers = parser.add_subparsers()
2321+
parser1 = subparsers.add_parser('bar')
2322+
parser1.add_argument('--fo')
2323+
parser1.add_argument('--foonew')
2324+
2325+
self.assertEqual(parser.parse_args(['--food', 'baz', 'bar']),
2326+
NS(foodle='baz', foonly=None, fo=None, foonew=None))
2327+
self.assertEqual(parser.parse_args(['--foon', 'baz', 'bar']),
2328+
NS(foodle=None, foonly='baz', fo=None, foonew=None))
2329+
self.assertArgumentParserError(parser.parse_args, ['--fo', 'baz', 'bar'])
2330+
self.assertEqual(parser.parse_args(['bar', '--fo', 'baz']),
2331+
NS(foodle=None, foonly=None, fo='baz', foonew=None))
2332+
self.assertEqual(parser.parse_args(['bar', '--foo', 'baz']),
2333+
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
2334+
self.assertEqual(parser.parse_args(['bar', '--foon', 'baz']),
2335+
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
2336+
self.assertArgumentParserError(parser.parse_args, ['bar', '--food', 'baz'])
2337+
23162338
def test_parse_known_args_with_single_dash_option(self):
23172339
parser = ErrorRaisingArgumentParser()
23182340
parser.add_argument('-k', '--known', action='count', default=0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix conflicts between abbreviated long options in the parent parser and
2+
subparsers in :mod:`argparse`.

0 commit comments

Comments
 (0)