Skip to content

Commit

Permalink
pythongh-85935: Check for nargs=0 for positional arguments in argparse
Browse files Browse the repository at this point in the history
Raise ValueError in add_argument() if either explicit nargs=0 or action
that does not consume arguments (like 'store_const' or 'store_true') is
specified for positional argument.
  • Loading branch information
serhiy-storchaka committed Oct 1, 2024
1 parent 8823690 commit f86e3ba
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 36 deletions.
10 changes: 9 additions & 1 deletion Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1426,11 +1426,17 @@ def add_argument(self, *args, **kwargs):
kwargs['default'] = self.argument_default

# create the action object, and add it to the parser
action_name = kwargs.get('action')
action_class = self._pop_action_class(kwargs)
if not callable(action_class):
raise ValueError('unknown action "%s"' % (action_class,))
action = action_class(**kwargs)

# raise an error if action for positional argument does not
# consume arguments
if not action.option_strings and action.nargs == 0:
raise ValueError(f'action {action_name!r} is not valid for positional arguments')

# raise an error if the action type is not callable
type_func = self._registry_get('type', action.type, action.type)
if not callable(type_func):
Expand Down Expand Up @@ -1534,7 +1540,9 @@ def _get_positional_kwargs(self, dest, **kwargs):
# mark positional arguments as required if at least one is
# always required
nargs = kwargs.get('nargs')
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS, 0]:
if nargs == 0:
raise ValueError('nargs for positionals must be != 0')
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS]:
kwargs['required'] = True

# return the keyword arguments with no option strings
Expand Down
80 changes: 45 additions & 35 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5262,15 +5262,15 @@ def custom_formatter(prog):
class TestInvalidArgumentConstructors(TestCase):
"""Test a bunch of invalid Argument constructors"""

def assertTypeError(self, *args, **kwargs):
def assertTypeError(self, *args, errmsg=None, **kwargs):
parser = argparse.ArgumentParser()
self.assertRaises(TypeError, parser.add_argument,
*args, **kwargs)
self.assertRaisesRegex(TypeError, errmsg, parser.add_argument,
*args, **kwargs)

def assertValueError(self, *args, **kwargs):
def assertValueError(self, *args, errmsg=None, **kwargs):
parser = argparse.ArgumentParser()
self.assertRaises(ValueError, parser.add_argument,
*args, **kwargs)
self.assertRaisesRegex(ValueError, errmsg, parser.add_argument,
*args, **kwargs)

def test_invalid_keyword_arguments(self):
self.assertTypeError('-x', bar=None)
Expand All @@ -5280,8 +5280,9 @@ def test_invalid_keyword_arguments(self):

def test_missing_destination(self):
self.assertTypeError()
for action in ['append', 'store']:
self.assertTypeError(action=action)
for action in ['store', 'append', 'extend']:
with self.subTest(action=action):
self.assertTypeError(action=action)

def test_invalid_option_strings(self):
self.assertValueError('--')
Expand All @@ -5298,10 +5299,8 @@ def test_invalid_action(self):
self.assertValueError('-x', action='foo')
self.assertValueError('foo', action='baz')
self.assertValueError('--foo', action=('store', 'append'))
parser = argparse.ArgumentParser()
with self.assertRaises(ValueError) as cm:
parser.add_argument("--foo", action="store-true")
self.assertIn('unknown action', str(cm.exception))
self.assertValueError('--foo', action="store-true",
errmsg='unknown action')

def test_multiple_dest(self):
parser = argparse.ArgumentParser()
Expand All @@ -5314,39 +5313,50 @@ def test_multiple_dest(self):
def test_no_argument_actions(self):
for action in ['store_const', 'store_true', 'store_false',
'append_const', 'count']:
for attrs in [dict(type=int), dict(nargs='+'),
dict(choices=['a', 'b'])]:
self.assertTypeError('-x', action=action, **attrs)
with self.subTest(action=action):
for attrs in [dict(type=int), dict(nargs='+'),
dict(choices=['a', 'b'])]:
with self.subTest(attrs=attrs):
self.assertTypeError('-x', action=action, **attrs)
self.assertTypeError('x', action=action, **attrs)
self.assertValueError('x', action=action,
errmsg=f"action '{action}' is not valid for positional arguments")
self.assertTypeError('-x', action=action, nargs=0)
self.assertValueError('x', action=action, nargs=0,
errmsg='nargs for positionals must be != 0')

def test_no_argument_no_const_actions(self):
# options with zero arguments
for action in ['store_true', 'store_false', 'count']:
with self.subTest(action=action):
# const is always disallowed
self.assertTypeError('-x', const='foo', action=action)

# const is always disallowed
self.assertTypeError('-x', const='foo', action=action)

# nargs is always disallowed
self.assertTypeError('-x', nargs='*', action=action)
# nargs is always disallowed
self.assertTypeError('-x', nargs='*', action=action)

def test_more_than_one_argument_actions(self):
for action in ['store', 'append']:

# nargs=0 is disallowed
self.assertValueError('-x', nargs=0, action=action)
self.assertValueError('spam', nargs=0, action=action)

# const is disallowed with non-optional arguments
for nargs in [1, '*', '+']:
self.assertValueError('-x', const='foo',
nargs=nargs, action=action)
self.assertValueError('spam', const='foo',
nargs=nargs, action=action)
for action in ['store', 'append', 'extend']:
with self.subTest(action=action):
# nargs=0 is disallowed
action_name = 'append' if action == 'extend' else action
self.assertValueError('-x', nargs=0, action=action,
errmsg=f'nargs for {action_name} actions must be != 0')
self.assertValueError('spam', nargs=0, action=action,
errmsg='nargs for positionals must be != 0')

# const is disallowed with non-optional arguments
for nargs in [1, '*', '+']:
self.assertValueError('-x', const='foo',
nargs=nargs, action=action)
self.assertValueError('spam', const='foo',
nargs=nargs, action=action)

def test_required_const_actions(self):
for action in ['store_const', 'append_const']:

# nargs is always disallowed
self.assertTypeError('-x', nargs='+', action=action)
with self.subTest(action=action):
# nargs is always disallowed
self.assertTypeError('-x', nargs='+', action=action)

def test_parsers_action_missing_params(self):
self.assertTypeError('command', action='parsers')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:meth:`argparse.ArgumentParser.add_argument` now raises exception if
:ref:`action` that does not consume arguments (like 'store_const' or
'strore_true') or explicit ``nargs=0`` are specified for positional
arguments.

0 comments on commit f86e3ba

Please sign in to comment.