mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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.
This commit is contained in:
parent
95e92ef6c7
commit
3f27153e07
3 changed files with 52 additions and 34 deletions
|
@ -1928,11 +1928,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
# otherwise, add the arg to the arg strings
|
# otherwise, add the arg to the arg strings
|
||||||
# and note the index if it was an option
|
# and note the index if it was an option
|
||||||
else:
|
else:
|
||||||
option_tuple = self._parse_optional(arg_string)
|
option_tuples = self._parse_optional(arg_string)
|
||||||
if option_tuple is None:
|
if option_tuples is None:
|
||||||
pattern = 'A'
|
pattern = 'A'
|
||||||
else:
|
else:
|
||||||
option_string_indices[i] = option_tuple
|
option_string_indices[i] = option_tuples
|
||||||
pattern = 'O'
|
pattern = 'O'
|
||||||
arg_string_pattern_parts.append(pattern)
|
arg_string_pattern_parts.append(pattern)
|
||||||
|
|
||||||
|
@ -1967,8 +1967,16 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
def consume_optional(start_index):
|
def consume_optional(start_index):
|
||||||
|
|
||||||
# get the optional identified at this index
|
# get the optional identified at this index
|
||||||
option_tuple = option_string_indices[start_index]
|
option_tuples = option_string_indices[start_index]
|
||||||
action, option_string, sep, explicit_arg = option_tuple
|
# if multiple actions match, the option string was ambiguous
|
||||||
|
if len(option_tuples) > 1:
|
||||||
|
options = ', '.join([option_string
|
||||||
|
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')
|
||||||
|
raise ArgumentError(None, msg % args)
|
||||||
|
|
||||||
|
action, option_string, sep, explicit_arg = option_tuples[0]
|
||||||
|
|
||||||
# identify additional optionals in the same arg string
|
# identify additional optionals in the same arg string
|
||||||
# (e.g. -xyz is the same as -x -y -z if no args are required)
|
# (e.g. -xyz is the same as -x -y -z if no args are required)
|
||||||
|
@ -2254,7 +2262,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
# if the option string is present in the parser, return the action
|
# if the option string is present in the parser, return the action
|
||||||
if arg_string in self._option_string_actions:
|
if arg_string in self._option_string_actions:
|
||||||
action = self._option_string_actions[arg_string]
|
action = self._option_string_actions[arg_string]
|
||||||
return action, arg_string, None, None
|
return [(action, arg_string, None, None)]
|
||||||
|
|
||||||
# if it's just a single character, it was meant to be positional
|
# if it's just a single character, it was meant to be positional
|
||||||
if len(arg_string) == 1:
|
if len(arg_string) == 1:
|
||||||
|
@ -2264,25 +2272,14 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
option_string, sep, explicit_arg = arg_string.partition('=')
|
option_string, sep, explicit_arg = arg_string.partition('=')
|
||||||
if sep and option_string in self._option_string_actions:
|
if sep and option_string in self._option_string_actions:
|
||||||
action = self._option_string_actions[option_string]
|
action = self._option_string_actions[option_string]
|
||||||
return action, option_string, sep, explicit_arg
|
return [(action, option_string, sep, explicit_arg)]
|
||||||
|
|
||||||
# search through all possible prefixes of the option string
|
# search through all possible prefixes of the option string
|
||||||
# and all actions in the parser for possible interpretations
|
# and all actions in the parser for possible interpretations
|
||||||
option_tuples = self._get_option_tuples(arg_string)
|
option_tuples = self._get_option_tuples(arg_string)
|
||||||
|
|
||||||
# if multiple actions match, the option string was ambiguous
|
if option_tuples:
|
||||||
if len(option_tuples) > 1:
|
return option_tuples
|
||||||
options = ', '.join([option_string
|
|
||||||
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')
|
|
||||||
raise ArgumentError(None, msg % args)
|
|
||||||
|
|
||||||
# if exactly one action matched, this segmentation is good,
|
|
||||||
# so return the parsed action
|
|
||||||
elif len(option_tuples) == 1:
|
|
||||||
option_tuple, = option_tuples
|
|
||||||
return option_tuple
|
|
||||||
|
|
||||||
# if it was not found as an option, but it looks like a negative
|
# if it was not found as an option, but it looks like a negative
|
||||||
# number, it was meant to be positional
|
# number, it was meant to be positional
|
||||||
|
@ -2297,7 +2294,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
|
|
||||||
# it was meant to be an optional but there is no such option
|
# 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)
|
# in this parser (though it might be a valid option in a subparser)
|
||||||
return None, arg_string, None, None
|
return [(None, arg_string, None, None)]
|
||||||
|
|
||||||
def _get_option_tuples(self, option_string):
|
def _get_option_tuples(self, option_string):
|
||||||
result = []
|
result = []
|
||||||
|
@ -2347,43 +2344,40 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
# in all examples below, we have to allow for '--' args
|
# in all examples below, we have to allow for '--' args
|
||||||
# which are represented as '-' in the pattern
|
# which are represented as '-' in the pattern
|
||||||
nargs = action.nargs
|
nargs = action.nargs
|
||||||
|
# if this is an optional action, -- is not allowed
|
||||||
|
option = action.option_strings
|
||||||
|
|
||||||
# the default (None) is assumed to be a single argument
|
# the default (None) is assumed to be a single argument
|
||||||
if nargs is None:
|
if nargs is None:
|
||||||
nargs_pattern = '(-*A-*)'
|
nargs_pattern = '([A])' if option else '(-*A-*)'
|
||||||
|
|
||||||
# allow zero or one arguments
|
# allow zero or one arguments
|
||||||
elif nargs == OPTIONAL:
|
elif nargs == OPTIONAL:
|
||||||
nargs_pattern = '(-*A?-*)'
|
nargs_pattern = '(A?)' if option else '(-*A?-*)'
|
||||||
|
|
||||||
# allow zero or more arguments
|
# allow zero or more arguments
|
||||||
elif nargs == ZERO_OR_MORE:
|
elif nargs == ZERO_OR_MORE:
|
||||||
nargs_pattern = '(-*[A-]*)'
|
nargs_pattern = '(A*)' if option else '(-*[A-]*)'
|
||||||
|
|
||||||
# allow one or more arguments
|
# allow one or more arguments
|
||||||
elif nargs == ONE_OR_MORE:
|
elif nargs == ONE_OR_MORE:
|
||||||
nargs_pattern = '(-*A[A-]*)'
|
nargs_pattern = '(A+)' if option else '(-*A[A-]*)'
|
||||||
|
|
||||||
# allow any number of options or arguments
|
# allow any number of options or arguments
|
||||||
elif nargs == REMAINDER:
|
elif nargs == REMAINDER:
|
||||||
nargs_pattern = '([-AO]*)'
|
nargs_pattern = '([AO]*)' if option else '(.*)'
|
||||||
|
|
||||||
# allow one argument followed by any number of options or arguments
|
# allow one argument followed by any number of options or arguments
|
||||||
elif nargs == PARSER:
|
elif nargs == PARSER:
|
||||||
nargs_pattern = '(-*A[-AO]*)'
|
nargs_pattern = '(A[AO]*)' if option else '(-*A[-AO]*)'
|
||||||
|
|
||||||
# suppress action, like nargs=0
|
# suppress action, like nargs=0
|
||||||
elif nargs == SUPPRESS:
|
elif nargs == SUPPRESS:
|
||||||
nargs_pattern = '(-*-*)'
|
nargs_pattern = '()' if option else '(-*)'
|
||||||
|
|
||||||
# all others should be integers
|
# all others should be integers
|
||||||
else:
|
else:
|
||||||
nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
|
nargs_pattern = '([AO]{%d})' % nargs if option else '((?:-*A){%d}-*)' % nargs
|
||||||
|
|
||||||
# if this is an optional action, -- is not allowed
|
|
||||||
if action.option_strings:
|
|
||||||
nargs_pattern = nargs_pattern.replace('-*', '')
|
|
||||||
nargs_pattern = nargs_pattern.replace('-', '')
|
|
||||||
|
|
||||||
# return the pattern
|
# return the pattern
|
||||||
return nargs_pattern
|
return nargs_pattern
|
||||||
|
|
|
@ -2356,6 +2356,28 @@ class TestAddSubparsers(TestCase):
|
||||||
self.assertEqual(C.w, 7)
|
self.assertEqual(C.w, 7)
|
||||||
self.assertEqual(C.x, 'b')
|
self.assertEqual(C.x, 'b')
|
||||||
|
|
||||||
|
def test_abbreviation(self):
|
||||||
|
parser = ErrorRaisingArgumentParser()
|
||||||
|
parser.add_argument('--foodle')
|
||||||
|
parser.add_argument('--foonly')
|
||||||
|
subparsers = parser.add_subparsers()
|
||||||
|
parser1 = subparsers.add_parser('bar')
|
||||||
|
parser1.add_argument('--fo')
|
||||||
|
parser1.add_argument('--foonew')
|
||||||
|
|
||||||
|
self.assertEqual(parser.parse_args(['--food', 'baz', 'bar']),
|
||||||
|
NS(foodle='baz', foonly=None, fo=None, foonew=None))
|
||||||
|
self.assertEqual(parser.parse_args(['--foon', 'baz', 'bar']),
|
||||||
|
NS(foodle=None, foonly='baz', fo=None, foonew=None))
|
||||||
|
self.assertArgumentParserError(parser.parse_args, ['--fo', 'baz', 'bar'])
|
||||||
|
self.assertEqual(parser.parse_args(['bar', '--fo', 'baz']),
|
||||||
|
NS(foodle=None, foonly=None, fo='baz', foonew=None))
|
||||||
|
self.assertEqual(parser.parse_args(['bar', '--foo', 'baz']),
|
||||||
|
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
|
||||||
|
self.assertEqual(parser.parse_args(['bar', '--foon', 'baz']),
|
||||||
|
NS(foodle=None, foonly=None, fo=None, foonew='baz'))
|
||||||
|
self.assertArgumentParserError(parser.parse_args, ['bar', '--food', 'baz'])
|
||||||
|
|
||||||
def test_parse_known_args_with_single_dash_option(self):
|
def test_parse_known_args_with_single_dash_option(self):
|
||||||
parser = ErrorRaisingArgumentParser()
|
parser = ErrorRaisingArgumentParser()
|
||||||
parser.add_argument('-k', '--known', action='count', default=0)
|
parser.add_argument('-k', '--known', action='count', default=0)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix conflicts between abbreviated long options in the parent parser and
|
||||||
|
subparsers in :mod:`argparse`.
|
Loading…
Add table
Add a link
Reference in a new issue