bpo-42973: argparse: mixing optional and positional by monkeyman79 · Pull Request #24259 · python/cpython · GitHub
Skip to content
Closed
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
24 changes: 24 additions & 0 deletions Doc/library/argparse.rst
28 changes: 22 additions & 6 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
'REMAINDER',
'SUPPRESS',
'ZERO_OR_MORE',
'AS_MANY_AS_POSSIBLE'
]


Expand All @@ -95,6 +96,7 @@

OPTIONAL = '?'
ZERO_OR_MORE = '*'
AS_MANY_AS_POSSIBLE = '**'
ONE_OR_MORE = '+'
PARSER = 'A...'
REMAINDER = '...'
Expand Down Expand Up @@ -590,7 +592,8 @@ def _format_args(self, action, default_metavar):
result = '%s' % get_metavar(1)
elif action.nargs == OPTIONAL:
result = '[%s]' % get_metavar(1)
elif action.nargs == ZERO_OR_MORE:
elif (action.nargs == ZERO_OR_MORE
or action.nargs == AS_MANY_AS_POSSIBLE):
metavar = get_metavar(1)
if len(metavar) == 2:
result = '[%s [%s ...]]' % metavar
Expand Down Expand Up @@ -692,7 +695,7 @@ def _get_help_string(self, action):
help = action.help
if '%(default)' not in action.help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE, AS_MANY_AS_POSSIBLE]
if action.option_strings or action.nargs in defaulting_nargs:
help += ' (default: %(default)s)'
return help
Expand Down Expand Up @@ -781,6 +784,7 @@ class Action(_AttributeHolder):
- N (an integer) consumes N arguments (and produces a list)
- '?' consumes zero or one arguments
- '*' consumes zero or more arguments (and produces a list)
- '**' consumes all remaining positional arguments
- '+' consumes one or more arguments (and produces a list)
Note that the difference between the default and nargs=1 is that
with the default, a single value will be produced, while with
Expand Down Expand Up @@ -1515,7 +1519,8 @@ def _get_positional_kwargs(self, dest, **kwargs):

# mark positional arguments as required if at least one is
# always required
if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]:
if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE,
AS_MANY_AS_POSSIBLE]:
kwargs['required'] = True
if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs:
kwargs['required'] = True
Expand All @@ -1541,6 +1546,11 @@ def _get_optional_kwargs(self, *args, **kwargs):
if len(option_string) > 1 and option_string[1] in self.prefix_chars:
long_option_strings.append(option_string)

# nargs='**' is invalid for optionals
if kwargs.get('nargs') == AS_MANY_AS_POSSIBLE:
msg = _("'nargs=\"**\"' is invalid for optionals")
raise TypeError(msg)

# infer destination, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
dest = kwargs.pop('dest', None)
if dest is None:
Expand Down Expand Up @@ -2010,17 +2020,23 @@ def consume_positionals(start_index):
match_partial = self._match_arguments_partial
selected_pattern = arg_strings_pattern[start_index:]
arg_counts = match_partial(positionals, selected_pattern)
action_index = 0

# slice off the appropriate arg strings for each Positional
# and add the Positional and its args to the list
for action, arg_count in zip(positionals, arg_counts):
for arg_count in arg_counts:
action = positionals[action_index]
args = arg_strings[start_index: start_index + arg_count]
start_index += arg_count
take_action(action, args)
# if positional action nargs is '**',
# never remove it from actions list
if action.nargs != AS_MANY_AS_POSSIBLE:
action_index += 1

# slice off the Positionals that we just parsed and return the
# index at which the Positionals' string args stopped
positionals[:] = positionals[len(arg_counts):]
positionals[:] = positionals[action_index:]
return start_index

# consume Positionals and Optionals alternately, until we have
Expand Down Expand Up @@ -2290,7 +2306,7 @@ def _get_nargs_pattern(self, action):
nargs_pattern = '(-*A?-*)'

# allow zero or more arguments
elif nargs == ZERO_OR_MORE:
elif nargs == ZERO_OR_MORE or nargs == AS_MANY_AS_POSSIBLE:
nargs_pattern = '(-*[A-]*)'

# allow one or more arguments
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5012,6 +5012,25 @@ def test_mixed(self):
self.assertEqual(NS(v=3, spam=True, badger="B"), args)
self.assertEqual(["C", "--foo", "4"], extras)

def test_nongreedy(self):
parser = argparse.ArgumentParser()
parser.add_argument('first', nargs='*', action='extend')
parser.add_argument('second')

argv = ['arg1', 'arg2', 'arg3']
args = parser.parse_args(argv)
self.assertEqual(NS(first=['arg1', 'arg2'], second='arg3'), args)

def test_greedy(self):
parser = argparse.ArgumentParser()
parser.add_argument('--foo', action='store')
parser.add_argument('first', nargs='**', action='extend')

argv = ['arg1', 'arg2', '--foo', 'bar', 'arg3', 'arg4']
args = parser.parse_args(argv)
self.assertEqual(NS(foo='bar',
first=['arg1', 'arg2', 'arg3', 'arg4']), args)

# ===========================
# parse_intermixed_args tests
# ===========================
Expand Down