https://github.com/python/cpython/commit/3f27153e077d7e9448e2f081275931968b40cc74
commit: 3f27153e077d7e9448e2f081275931968b40cc74
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2024-09-29T12:01:03+03:00
summary:
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.
files:
A Misc/NEWS.d/next/Library/2024-09-26-22-14-12.gh-issue-58573.hozbm9.rst
M Lib/argparse.py
M Lib/test/test_argparse.py
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 504289192f3f96..874f271959c4fe 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -1928,11 +1928,11 @@ def _parse_known_args(self, arg_strings, namespace):
# otherwise, add the arg to the arg strings
# and note the index if it was an option
else:
- option_tuple = self._parse_optional(arg_string)
- if option_tuple is None:
+ option_tuples = self._parse_optional(arg_string)
+ if option_tuples is None:
pattern = 'A'
else:
- option_string_indices[i] = option_tuple
+ option_string_indices[i] = option_tuples
pattern = 'O'
arg_string_pattern_parts.append(pattern)
@@ -1967,8 +1967,16 @@ def take_action(action, argument_strings,
option_string=None):
def consume_optional(start_index):
# get the optional identified at this index
- option_tuple = option_string_indices[start_index]
- action, option_string, sep, explicit_arg = option_tuple
+ option_tuples = option_string_indices[start_index]
+ # 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
# (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):
# if the option string is present in the parser, return the action
if arg_string in self._option_string_actions:
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 len(arg_string) == 1:
@@ -2264,25 +2272,14 @@ def _parse_optional(self, arg_string):
option_string, sep, explicit_arg = arg_string.partition('=')
if sep and option_string in self._option_string_actions:
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
# and all actions in the parser for possible interpretations
option_tuples = self._get_option_tuples(arg_string)
- # 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)
-
- # 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 option_tuples:
+ return option_tuples
# if it was not found as an option, but it looks like a negative
# number, it was meant to be positional
@@ -2297,7 +2294,7 @@ def _parse_optional(self, arg_string):
# 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)
- return None, arg_string, None, None
+ return [(None, arg_string, None, None)]
def _get_option_tuples(self, option_string):
result = []
@@ -2347,43 +2344,40 @@ def _get_nargs_pattern(self, action):
# in all examples below, we have to allow for '--' args
# which are represented as '-' in the pattern
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
if nargs is None:
- nargs_pattern = '(-*A-*)'
+ nargs_pattern = '([A])' if option else '(-*A-*)'
# allow zero or one arguments
elif nargs == OPTIONAL:
- nargs_pattern = '(-*A?-*)'
+ nargs_pattern = '(A?)' if option else '(-*A?-*)'
# allow zero or more arguments
elif nargs == ZERO_OR_MORE:
- nargs_pattern = '(-*[A-]*)'
+ nargs_pattern = '(A*)' if option else '(-*[A-]*)'
# allow one or more arguments
elif nargs == ONE_OR_MORE:
- nargs_pattern = '(-*A[A-]*)'
+ nargs_pattern = '(A+)' if option else '(-*A[A-]*)'
# allow any number of options or arguments
elif nargs == REMAINDER:
- nargs_pattern = '([-AO]*)'
+ nargs_pattern = '([AO]*)' if option else '(.*)'
# allow one argument followed by any number of options or arguments
elif nargs == PARSER:
- nargs_pattern = '(-*A[-AO]*)'
+ nargs_pattern = '(A[AO]*)' if option else '(-*A[-AO]*)'
# suppress action, like nargs=0
elif nargs == SUPPRESS:
- nargs_pattern = '(-*-*)'
+ nargs_pattern = '()' if option else '(-*)'
# all others should be integers
else:
- nargs_pattern = '(-*%s-*)' % '-*'.join('A' * nargs)
-
- # if this is an optional action, -- is not allowed
- if action.option_strings:
- nargs_pattern = nargs_pattern.replace('-*', '')
- nargs_pattern = nargs_pattern.replace('-', '')
+ nargs_pattern = '([AO]{%d})' % nargs if option else
'((?:-*A){%d}-*)' % nargs
# return the pattern
return nargs_pattern
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index 15805937baaa97..a972ed0cc9053b 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -2356,6 +2356,28 @@ class C:
self.assertEqual(C.w, 7)
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):
parser = ErrorRaisingArgumentParser()
parser.add_argument('-k', '--known', action='count', default=0)
diff --git
a/Misc/NEWS.d/next/Library/2024-09-26-22-14-12.gh-issue-58573.hozbm9.rst
b/Misc/NEWS.d/next/Library/2024-09-26-22-14-12.gh-issue-58573.hozbm9.rst
new file mode 100644
index 00000000000000..37d64ee536ff49
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-09-26-22-14-12.gh-issue-58573.hozbm9.rst
@@ -0,0 +1,2 @@
+Fix conflicts between abbreviated long options in the parent parser and
+subparsers in :mod:`argparse`.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]