Skip to content

Commit

Permalink
Handle more corner cases.
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka committed Jan 17, 2024
1 parent e05f2b1 commit fadc5fd
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 27 deletions.
50 changes: 28 additions & 22 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2005,7 +2005,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 @@ -2032,17 +2032,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:
extras.append(option_string)
explicit_arg = new_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 @@ -2262,18 +2272,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 @@ -2282,7 +2291,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 @@ -2306,7 +2315,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 @@ -2316,34 +2325,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
31 changes: 26 additions & 5 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2275,11 +2275,32 @@ def test_parse_known_args(self):
)

def test_parse_known_args_with_single_dash_option(self):
parser = argparse.ArgumentParser()
parser.add_argument('-k', '--known', action='store_true')
self.assertEqual(parser.parse_known_args(['-k', '-u']), (NS(known=True), ['-u']))
self.assertEqual(parser.parse_known_args(['-ku']), (NS(known=True), ['-u']))
self.assertEqual(parser.parse_known_args(['-uk']), (NS(known=False), ['-uk']))
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()
Expand Down

0 comments on commit fadc5fd

Please sign in to comment.