summaryrefslogtreecommitdiffstats
path: root/Lib/argparse.py
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-10-22 10:57:25 (GMT)
committerGitHub <noreply@github.com>2024-10-22 10:57:25 (GMT)
commit759a54d28ffe7eac8c23917f5d3dfad8309856be (patch)
tree17102747f71a1597192c16963e4b07c20727b612 /Lib/argparse.py
parent57e3c59bb64fc2f8b2845a7e03ab0abb029ccd02 (diff)
downloadcpython-759a54d28ffe7eac8c23917f5d3dfad8309856be.zip
cpython-759a54d28ffe7eac8c23917f5d3dfad8309856be.tar.gz
cpython-759a54d28ffe7eac8c23917f5d3dfad8309856be.tar.bz2
gh-125355: Rewrite parse_intermixed_args() in argparse (GH-125356)
* The parser no longer changes temporarily during parsing. * Default values are not processed twice. * Required mutually exclusive groups containing positional arguments are now supported. * The missing arguments report now includes the names of all required optional and positional arguments. * Unknown options can be intermixed with positional arguments in parse_known_intermixed_args().
Diffstat (limited to 'Lib/argparse.py')
-rw-r--r--Lib/argparse.py99
1 files changed, 34 insertions, 65 deletions
diff --git a/Lib/argparse.py b/Lib/argparse.py
index 49271a1..024622b 100644
--- a/Lib/argparse.py
+++ b/Lib/argparse.py
@@ -1924,6 +1924,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
return args
def parse_known_args(self, args=None, namespace=None):
+ return self._parse_known_args2(args, namespace, intermixed=False)
+
+ def _parse_known_args2(self, args, namespace, intermixed):
if args is None:
# args default to the system args
args = _sys.argv[1:]
@@ -1950,18 +1953,18 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# parse the arguments and exit if there are any errors
if self.exit_on_error:
try:
- namespace, args = self._parse_known_args(args, namespace)
+ namespace, args = self._parse_known_args(args, namespace, intermixed)
except ArgumentError as err:
self.error(str(err))
else:
- namespace, args = self._parse_known_args(args, namespace)
+ namespace, args = self._parse_known_args(args, namespace, intermixed)
if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR):
args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR))
delattr(namespace, _UNRECOGNIZED_ARGS_ATTR)
return namespace, args
- def _parse_known_args(self, arg_strings, namespace):
+ def _parse_known_args(self, arg_strings, namespace, intermixed):
# replace arg strings that are file references
if self.fromfile_prefix_chars is not None:
arg_strings = self._read_args_from_files(arg_strings)
@@ -2052,6 +2055,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# if we found no optional action, skip it
if action is None:
extras.append(arg_strings[start_index])
+ extras_pattern.append('O')
return start_index + 1
# if there is an explicit argument, try to match the
@@ -2087,6 +2091,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
sep = ''
else:
extras.append(char + explicit_arg)
+ extras_pattern.append('O')
stop = start_index + 1
break
# if the action expect exactly one argument, we've
@@ -2165,6 +2170,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# consume Positionals and Optionals alternately, until we have
# passed the last option string
extras = []
+ extras_pattern = []
start_index = 0
if option_string_indices:
max_option_string_index = max(option_string_indices)
@@ -2178,7 +2184,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
if next_option_string_index in option_string_indices:
break
next_option_string_index += 1
- if start_index != next_option_string_index:
+ if not intermixed and start_index != next_option_string_index:
positionals_end_index = consume_positionals(start_index)
# only try to parse the next optional if we didn't consume
@@ -2194,16 +2200,35 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
if start_index not in option_string_indices:
strings = arg_strings[start_index:next_option_string_index]
extras.extend(strings)
+ extras_pattern.extend(arg_strings_pattern[start_index:next_option_string_index])
start_index = next_option_string_index
# consume the next optional and any arguments for it
start_index = consume_optional(start_index)
- # consume any positionals following the last Optional
- stop_index = consume_positionals(start_index)
+ if not intermixed:
+ # consume any positionals following the last Optional
+ stop_index = consume_positionals(start_index)
- # if we didn't consume all the argument strings, there were extras
- extras.extend(arg_strings[stop_index:])
+ # if we didn't consume all the argument strings, there were extras
+ extras.extend(arg_strings[stop_index:])
+ else:
+ extras.extend(arg_strings[start_index:])
+ extras_pattern.extend(arg_strings_pattern[start_index:])
+ extras_pattern = ''.join(extras_pattern)
+ assert len(extras_pattern) == len(extras)
+ # consume all positionals
+ arg_strings = [s for s, c in zip(extras, extras_pattern) if c != 'O']
+ arg_strings_pattern = extras_pattern.replace('O', '')
+ stop_index = consume_positionals(0)
+ # leave unknown optionals and non-consumed positionals in extras
+ for i, c in enumerate(extras_pattern):
+ if not stop_index:
+ break
+ if c != 'O':
+ stop_index -= 1
+ extras[i] = None
+ extras = [s for s in extras if s is not None]
# make sure all required actions were present and also convert
# action defaults which were not given as arguments
@@ -2469,10 +2494,6 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# are then parsed. If the parser definition is incompatible with the
# intermixed assumptions (e.g. use of REMAINDER, subparsers) a
# TypeError is raised.
- #
- # positionals are 'deactivated' by setting nargs and default to
- # SUPPRESS. This blocks the addition of that positional to the
- # namespace
positionals = self._get_positional_actions()
a = [action for action in positionals
@@ -2481,59 +2502,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
raise TypeError('parse_intermixed_args: positional arg'
' with nargs=%s'%a[0].nargs)
- if [action.dest for group in self._mutually_exclusive_groups
- for action in group._group_actions if action in positionals]:
- raise TypeError('parse_intermixed_args: positional in'
- ' mutuallyExclusiveGroup')
-
- try:
- save_usage = self.usage
- try:
- if self.usage is None:
- # capture the full usage for use in error messages
- self.usage = self.format_usage()[7:]
- for action in positionals:
- # deactivate positionals
- action.save_nargs = action.nargs
- # action.nargs = 0
- action.nargs = SUPPRESS
- action.save_default = action.default
- action.default = SUPPRESS
- namespace, remaining_args = self.parse_known_args(args,
- namespace)
- for action in positionals:
- # remove the empty positional values from namespace
- if (hasattr(namespace, action.dest)
- and getattr(namespace, action.dest)==[]):
- from warnings import warn
- warn('Do not expect %s in %s' % (action.dest, namespace))
- delattr(namespace, action.dest)
- finally:
- # restore nargs and usage before exiting
- for action in positionals:
- action.nargs = action.save_nargs
- action.default = action.save_default
- optionals = self._get_optional_actions()
- try:
- # parse positionals. optionals aren't normally required, but
- # they could be, so make sure they aren't.
- for action in optionals:
- action.save_required = action.required
- action.required = False
- for group in self._mutually_exclusive_groups:
- group.save_required = group.required
- group.required = False
- namespace, extras = self.parse_known_args(remaining_args,
- namespace)
- finally:
- # restore parser values before exiting
- for action in optionals:
- action.required = action.save_required
- for group in self._mutually_exclusive_groups:
- group.required = group.save_required
- finally:
- self.usage = save_usage
- return namespace, extras
+ return self._parse_known_args2(args, namespace, intermixed=True)
# ========================
# Value conversion methods