8000 gh-58573: Fix conflicts between abbreviated long options in the paren… · python/cpython@3f27153 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3f27153

Browse files
gh-58573: Fix conflicts between abbreviated long options in the parent parser and subparsers in argparse (GH-124631)
Check for ambiguous options if the option is consumed, not when it is parsed.
1 parent 95e92ef commit 3f27153

File tree

3 files changed

+52
-34
lines changed

3 files changed

+52
-34
lines changed

Lib/argparse.py

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,11 +1928,11 @@ def _parse_known_args(self, arg_strings, namespace):
19281928
# otherwise, add the arg to the arg strings
19291929
# and note the index if it was an option
19301930
else:
1931-
option_tuple = self._parse_optional(arg_string)
1932-
if option_tuple is None:
1931+
option_tuples = self._parse_optional(arg_string)
1932+
if option_tuples is None:
19331933
pattern = 'A'
19341934
else:
1935-
option_string_indices[i] = option_tuple
1935+
option_string_indices[i] = option_tuples
19361936
pattern = 'O'
19371937
arg_string_pattern_parts.append(pattern)
19381938

@@ -1967,8 +1967,16 @@ def take_action(action, argument_strings, option_string=None):
19671967
def consume_optional(start_index):
19681968

19691969
# get the optional identified at this index
1970-
option_tuple = option_string_indices[start_index]
1971-
action, option_string, sep, explicit_arg = option_tuple
1970+
option_tuples = option_string_indices[start_index]
1971+
# if multiple actions match, the option string was ambiguous
1972+
if len(option_tuples) > 1:
1973+
options = ', '.join([option_string
1974+
for action, option_string, sep, explicit_arg in option_tuples])
1975+
args = {'option': arg_string, 'matches': options}
1976+
msg = _('ambiguous option: %(option)s could match %(matches)s')
1977+
raise ArgumentError(None, msg % args)
1978+
1979+
action, option_string, sep, explicit_arg = option_tuples[0]
19721980

19731981
# identify additional optionals in the same arg string
19741982
# (e.g. -xyz is the same as -x -y -z if no args are required)
@@ -2254,7 +2262,7 @@ def _parse_optional(self, arg_string):
22542262
# if the option string is present in the parser, return the action
22552263
if arg_string in self._option_string_actions:
22562264
action = self._option_string_actions[arg_string]
2257-
return action, arg_string, None, None
2265+
return [(action, arg_string, None, None)]
22582266

22592267
# if it's just a single character, it was meant to be positional
22602268
if len(arg_string) == 1:
@@ -2264,25 +2272,14 @@ def _parse_optional(self, arg_string):
22642272
option_string, sep, explicit_arg = arg_string.partition('=')
22652273
if sep and option_string in self._option_string_actions:
22662274
action = self._option_string_actions[option_string]
2267-
return action, option_string, sep, explicit_arg
2275+
return [(action, option_string, sep, explicit_arg)]
22682276

22692277
# search through all possible prefixes of the option string
22702278
# and all actions in the parser for possible interpretations
22712279
option_tuples = self._get_option_tuples(arg_string)
22722280

2273-
# if multiple actions match, the option string was ambiguous
2274-
if len(option_tuples) > 1:
2275-
options = ', '.join([option_string
2276-
for action, option_string, sep, explicit_arg in option_tuples])
2277-
args = {'option': arg_string, 'matches': options}
2278-
msg = _('ambiguous option: %(option)s could match %(matches)s')
2279-
raise ArgumentError(None, msg % args)
2280-
2281-
# if exactly one action matched, this segmentation is good,
2282-
# so return the parsed action
2283-
elif len(option_tuples) == 1:
2284-
option_tuple, = option_tuples
2285-
return option_tuple
2281+
if option_tuples:
2282+
return option_tuples
22862283

22872284
# if it was not found as an option, but it looks like a negative
22882285
# number, it was meant to be positional
@@ -2297,7 +2294,7 @@ def _parse_optional(self, arg_string):
22972294

22982295
# it was meant to be an optional but there is no such option
22992296
# in this parser (though it might be a valid option in a subparser)
2300-
return None, arg_string, None, None
2297+
return [(None, arg_string, None, None)]
23012298

23022299
def _get_option_tuples(self, option_string):
23032300
result = []
@@ -2347,43 +2344,40 @@ def _get_nargs_pattern(self, action):
23472344
# in all examples below, we have to allow for '--' args
23482345
# which are represented as '-' in the pattern
23492346
nargs = action.nargs
2347+
# if this is an optional action, -- is not allowed
2348+
option = action.option_strings
23502349

23512350
# the default (None) is assumed to be a single argument
23522351
if nargs is None:
2353-
nargs_pattern = '(-*A-*)'
2352+
nargs_pattern = '([A])' if option else '(-*A-*)'
23542353

23552354
# allow zero or one arguments
23562355
elif nargs == OPTIONAL:
2357-
nargs_pattern = '(-*A?-*)'
2356+
nargs_pattern = '(A?)' if option else '(-*A?-*)'
23582357

23592358
# allow zero or more arguments
23602359
elif nargs == ZERO_OR_MORE:
2361-
nargs_pattern = '(-*[A-]*)'
2360+
nargs_pattern = '(A*)' if option else '(-*[A-]*)'
23622361

23632362
# allow one or more arguments
23642363
elif nargs == ONE_OR_MORE:
2365-
nargs_pattern = '(-*A[A-]*)'
2364+
nargs_pattern = '(A+)' if option else '(-*A[A-]*)'
23662365

23672366
# allow any number of options or arguments
23682367
elif nargs == REMAINDER:
2369-
nargs_pattern = '([-AO]*)'
2368+
nargs_pattern = '([AO]*)' if option else '(.*)'
23702369

23712370
# allow one argument followed by any number of options or arguments
23722371
elif nargs == PARSER:
2373-
nargs_pattern = '(-*A[-AO]*)'
2372+
nargs_pattern = '(A[AO]*)' if option else '(-*A[-AO]*)'
23742373

23752374
# suppress action, like nargs=0
23762375
elif nargs == SUPPRESS:
2377-
nargs_pattern = '(-*-*)'
2376+
nargs_pattern = '()' if option else '(-*)'
23782377

23792378
# all others should be integers
23802379
else:
2381-
nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
2382-
2383-
# if this is an optional action, -- is not allowed
2384-
if action.option_strings:
2385-
nargs_pattern = nargs_pattern.replace('-*', '')
2386-
nargs_pattern = nargs_pattern.replace('-', '')
2380+
nargs_pattern = '([AO]{%d})' % nargs if option else '((?:-*A){%d}-*)' % nargs
23872381

23882382
# return the pattern
23892383
return nargs_pattern

Lib/test/test_argparse.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,28 @@ class C:
23562356
self.assertEqual(C.w, 7)
23572357
self.assertEqual(C.x, 'b')
23582358

2359+
def test_abbreviation(self):
2360+
parser = ErrorRaisingArgumentParser()
2361+
parser.add_argument('--foodle')
2362+
parser.add_argument('--foonly')
2363+
subparsers = parser.add_subparsers()
2364+
parser1 = subparsers.add_parser('bar')
2365+
parser1.add_argument('--fo')
2366+
parser1.add_argument('--foonew')
2367+
2368+
self.assertEqual(parser.parse_args(['--food', 'baz', 'bar']),
2369+
NS(foodle='baz', foonly=None, fo=None, foonew=None))
2370+
self.assertEqual(parser.parse_args(['--foon', 'baz', 'bar']),
2371+
NS(foodle=None, foonly='baz', fo=None, foonew=None))
2372+
self.assertArgumentParserError(parser.parse_args, ['--fo', 'baz', 'bar'])
2373+
self.assertEqual(parser.parse_args(['bar', '--fo', 'baz']),
2374+
NS(foodle=None, foonly=None, fo='baz', foonew=None))
2375+
self.assertEqual(parser.parse_args(['bar', '--foo', 'baz']),
2376+
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
2377+
self.assertEqual(parser.parse_args(['bar', '--foon', 'baz']),
2378+
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
2379+
self.assertArgumentParserError(parser.parse_args, ['bar', '--food', 'baz'])
2380+
23592381
def test_parse_known_args_with_single_dash_option(self):
23602382
parser = ErrorRaisingArgumentParser()
23612383
parser.add_argument('-k', '--known', action='count', default=0)
Lines changed: 2 additions & 0 deletions
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)
0