Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.11] gh-60346: Improve handling single-dash options in ArgumentParser.parse_known_args() (GH-114180) #115674

Merged
merged 1 commit into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1982,7 +1982,7 @@ def consume_optional(start_index):

# get the optional identified at this index
option_tuple = option_string_indices[start_index]
action, option_string, explicit_arg = option_tuple
action, option_string, sep, explicit_arg = option_tuple

# identify additional optionals in the same arg string
# (e.g. -xyz is the same as -x -y -z if no args are required)
Expand All @@ -2009,18 +2009,27 @@ def consume_optional(start_index):
and option_string[1] not in chars
and explicit_arg != ''
):
if sep or explicit_arg[0] in chars:
msg = _('ignored explicit argument %r')
raise ArgumentError(action, msg % explicit_arg)
action_tuples.append((action, [], option_string))
char = option_string[0]
option_string = char + explicit_arg[0]
new_explicit_arg = explicit_arg[1:] or None
optionals_map = self._option_string_actions
if option_string in optionals_map:
action = optionals_map[option_string]
explicit_arg = new_explicit_arg
explicit_arg = explicit_arg[1:]
if not explicit_arg:
sep = explicit_arg = None
elif explicit_arg[0] == '=':
sep = '='
explicit_arg = explicit_arg[1:]
else:
sep = ''
else:
msg = _('ignored explicit argument %r')
raise ArgumentError(action, msg % explicit_arg)

extras.append(char + explicit_arg)
stop = start_index + 1
break
# if the action expect exactly one argument, we've
# successfully matched the option; exit the loop
elif arg_count == 1:
Expand Down Expand Up @@ -2238,18 +2247,17 @@ 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
return action, arg_string, None, None

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

# if the option string before the "=" is present, return the action
if '=' in arg_string:
option_string, explicit_arg = arg_string.split('=', 1)
if option_string in self._option_string_actions:
action = self._option_string_actions[option_string]
return action, option_string, explicit_arg
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

# search through all possible prefixes of the option string
# and all actions in the parser for possible interpretations
Expand All @@ -2258,7 +2266,7 @@ def _parse_optional(self, arg_string):
# if multiple actions match, the option string was ambiguous
if len(option_tuples) > 1:
options = ', '.join([option_string
for action, option_string, explicit_arg in option_tuples])
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')
self.error(msg % args)
Expand All @@ -2282,7 +2290,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
return None, arg_string, None, None

def _get_option_tuples(self, option_string):
result = []
Expand All @@ -2292,34 +2300,31 @@ def _get_option_tuples(self, option_string):
chars = self.prefix_chars
if option_string[0] in chars and option_string[1] in chars:
if self.allow_abbrev:
if '=' in option_string:
option_prefix, explicit_arg = option_string.split('=', 1)
else:
option_prefix = option_string
explicit_arg = None
option_prefix, sep, explicit_arg = option_string.partition('=')
if not sep:
sep = explicit_arg = None
for option_string in self._option_string_actions:
if option_string.startswith(option_prefix):
action = self._option_string_actions[option_string]
tup = action, option_string, explicit_arg
tup = action, option_string, sep, explicit_arg
result.append(tup)

# single character options can be concatenated with their arguments
# but multiple character options always have to have their argument
# separate
elif option_string[0] in chars and option_string[1] not in chars:
option_prefix = option_string
explicit_arg = None
short_option_prefix = option_string[:2]
short_explicit_arg = option_string[2:]

for option_string in self._option_string_actions:
if option_string == short_option_prefix:
action = self._option_string_actions[option_string]
tup = action, option_string, short_explicit_arg
tup = action, option_string, '', short_explicit_arg
result.append(tup)
elif option_string.startswith(option_prefix):
action = self._option_string_actions[option_string]
tup = action, option_string, explicit_arg
tup = action, option_string, None, None
result.append(tup)

# shouldn't ever get here
Expand Down
28 changes: 28 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2156,6 +2156,34 @@ def test_parse_known_args(self):
(NS(foo=False, bar=0.5, w=7, x='b'), ['-W', '-X', 'Y', 'Z']),
)

def test_parse_known_args_with_single_dash_option(self):
parser = ErrorRaisingArgumentParser()
parser.add_argument('-k', '--known', action='count', default=0)
parser.add_argument('-n', '--new', action='count', default=0)
self.assertEqual(parser.parse_known_args(['-k', '-u']),
(NS(known=1, new=0), ['-u']))
self.assertEqual(parser.parse_known_args(['-u', '-k']),
(NS(known=1, new=0), ['-u']))
self.assertEqual(parser.parse_known_args(['-ku']),
(NS(known=1, new=0), ['-u']))
self.assertArgumentParserError(parser.parse_known_args, ['-k=u'])
self.assertEqual(parser.parse_known_args(['-uk']),
(NS(known=0, new=0), ['-uk']))
self.assertEqual(parser.parse_known_args(['-u=k']),
(NS(known=0, new=0), ['-u=k']))
self.assertEqual(parser.parse_known_args(['-kunknown']),
(NS(known=1, new=0), ['-unknown']))
self.assertArgumentParserError(parser.parse_known_args, ['-k=unknown'])
self.assertEqual(parser.parse_known_args(['-ku=nknown']),
(NS(known=1, new=0), ['-u=nknown']))
self.assertEqual(parser.parse_known_args(['-knew']),
(NS(known=1, new=1), ['-ew']))
self.assertArgumentParserError(parser.parse_known_args, ['-kn=ew'])
self.assertArgumentParserError(parser.parse_known_args, ['-k-new'])
self.assertArgumentParserError(parser.parse_known_args, ['-kn-ew'])
self.assertEqual(parser.parse_known_args(['-kne-w']),
(NS(known=1, new=1), ['-e-w']))

def test_dest(self):
parser = ErrorRaisingArgumentParser()
parser.add_argument('--foo', action='store_true')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ArgumentParser inconsistent with parse_known_args.
Loading