summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-02-19 17:20:00 (GMT)
committerGitHub <noreply@github.com>2024-02-19 17:20:00 (GMT)
commite47ecbd0420528f1f9f282d9e7acfcf586a4caa1 (patch)
tree27d644460e8841dec5b7b543ee033a065b1cf214
parent872cc9957a9c8b971448e7377fad865f351da6c9 (diff)
downloadcpython-e47ecbd0420528f1f9f282d9e7acfcf586a4caa1.zip
cpython-e47ecbd0420528f1f9f282d9e7acfcf586a4caa1.tar.gz
cpython-e47ecbd0420528f1f9f282d9e7acfcf586a4caa1.tar.bz2
gh-60346: Improve handling single-dash options in ArgumentParser.parse_known_args() (GH-114180)
-rw-r--r--Lib/argparse.py51
-rw-r--r--Lib/test/test_argparse.py28
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst1
3 files changed, 57 insertions, 23 deletions
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 04ee3b1..6ef0bea 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -2033,7 +2033,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# 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)
@@ -2060,18 +2060,27 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
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:
@@ -2299,18 +2308,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# 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
@@ -2319,7 +2327,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# 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)
@@ -2343,7 +2351,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# 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 = []
@@ -2353,15 +2361,13 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
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
@@ -2369,18 +2375,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# 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
diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py
index 86d6e81..65fd9cf 100644
--- a/Lib/test/test_argparse.py
+++ b/Lib/test/test_argparse.py
@@ -2274,6 +2274,34 @@ class TestAddSubparsers(TestCase):
(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')
diff --git a/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst b/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst
new file mode 100644
index 0000000..c15bd6e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-02-21-20-35.gh-issue-60346.7mjgua.rst
@@ -0,0 +1 @@
+Fix ArgumentParser inconsistent with parse_known_args.