Skip to content

Commit f86e3ba

Browse files
pythongh-85935: Check for nargs=0 for positional arguments in argparse
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.
1 parent 8823690 commit f86e3ba

File tree

3 files changed

+58
-36
lines changed

3 files changed

+58
-36
lines changed

Lib/argparse.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1426,11 +1426,17 @@ def add_argument(self, *args, **kwargs):
14261426
kwargs['default'] = self.argument_default
14271427

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

1435+
# raise an error if action for positional argument does not
1436+
# consume arguments
1437+
if not action.option_strings and action.nargs == 0:
1438+
raise ValueError(f'action {action_name!r} is not valid for positional arguments')
1439+
14341440
# raise an error if the action type is not callable
14351441
type_func = self._registry_get('type', action.type, action.type)
14361442
if not callable(type_func):
@@ -1534,7 +1540,9 @@ def _get_positional_kwargs(self, dest, **kwargs):
15341540
# mark positional arguments as required if at least one is
15351541
# always required
15361542
nargs = kwargs.get('nargs')
1537-
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS, 0]:
1543+
if nargs == 0:
1544+
raise ValueError('nargs for positionals must be != 0')
1545+
if nargs not in [OPTIONAL, ZERO_OR_MORE, REMAINDER, SUPPRESS]:
15381546
kwargs['required'] = True
15391547

15401548
# return the keyword arguments with no option strings

Lib/test/test_argparse.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5262,15 +5262,15 @@ def custom_formatter(prog):
52625262
class TestInvalidArgumentConstructors(TestCase):
52635263
"""Test a bunch of invalid Argument constructors"""
52645264

5265-
def assertTypeError(self, *args, **kwargs):
5265+
def assertTypeError(self, *args, errmsg=None, **kwargs):
52665266
parser = argparse.ArgumentParser()
5267-
self.assertRaises(TypeError, parser.add_argument,
5268-
*args, **kwargs)
5267+
self.assertRaisesRegex(TypeError, errmsg, parser.add_argument,
5268+
*args, **kwargs)
52695269

5270-
def assertValueError(self, *args, **kwargs):
5270+
def assertValueError(self, *args, errmsg=None, **kwargs):
52715271
parser = argparse.ArgumentParser()
5272-
self.assertRaises(ValueError, parser.add_argument,
5273-
*args, **kwargs)
5272+
self.assertRaisesRegex(ValueError, errmsg, parser.add_argument,
5273+
*args, **kwargs)
52745274

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

52815281
def test_missing_destination(self):
52825282
self.assertTypeError()
5283-
for action in ['append', 'store']:
5284-
self.assertTypeError(action=action)
5283+
for action in ['store', 'append', 'extend']:
5284+
with self.subTest(action=action):
5285+
self.assertTypeError(action=action)
52855286

52865287
def test_invalid_option_strings(self):
52875288
self.assertValueError('--')
@@ -5298,10 +5299,8 @@ def test_invalid_action(self):
52985299
self.assertValueError('-x', action='foo')
52995300
self.assertValueError('foo', action='baz')
53005301
self.assertValueError('--foo', action=('store', 'append'))
5301-
parser = argparse.ArgumentParser()
5302-
with self.assertRaises(ValueError) as cm:
5303-
parser.add_argument("--foo", action="store-true")
5304-
self.assertIn('unknown action', str(cm.exception))
5302+
self.assertValueError('--foo', action="store-true",
5303+
errmsg='unknown action')
53055304

53065305
def test_multiple_dest(self):
53075306
parser = argparse.ArgumentParser()
@@ -5314,39 +5313,50 @@ def test_multiple_dest(self):
53145313
def test_no_argument_actions(self):
53155314
for action in ['store_const', 'store_true', 'store_false',
53165315
'append_const', 'count']:
5317-
for attrs in [dict(type=int), dict(nargs='+'),
5318-
dict(choices=['a', 'b'])]:
5319-
self.assertTypeError('-x', action=action, **attrs)
5316+
with self.subTest(action=action):
5317+
for attrs in [dict(type=int), dict(nargs='+'),
5318+
dict(choices=['a', 'b'])]:
5319+
with self.subTest(attrs=attrs):
5320+
self.assertTypeError('-x', action=action, **attrs)
5321+
self.assertTypeError('x', action=action, **attrs)
5322+
self.assertValueError('x', action=action,
5323+
errmsg=f"action '{action}' is not valid for positional arguments")
5324+
self.assertTypeError('-x', action=action, nargs=0)
5325+
self.assertValueError('x', action=action, nargs=0,
5326+
errmsg='nargs for positionals must be != 0')
53205327

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

5325-
# const is always disallowed
5326-
self.assertTypeError('-x', const='foo', action=action)
5327-
5328-
# nargs is always disallowed
5329-
self.assertTypeError('-x', nargs='*', action=action)
5335+
# nargs is always disallowed
5336+
self.assertTypeError('-x', nargs='*', action=action)
53305337

53315338
def test_more_than_one_argument_actions(self):
5332-
for action in ['store', 'append']:
5333-
5334-
# nargs=0 is disallowed
5335-
self.assertValueError('-x', nargs=0, action=action)
5336-
self.assertValueError('spam', nargs=0, action=action)
5337-
5338-
# const is disallowed with non-optional arguments
5339-
for nargs in [1, '*', '+']:
5340-
self.assertValueError('-x', const='foo',
5341-
nargs=nargs, action=action)
5342-
self.assertValueError('spam', const='foo',
5343-
nargs=nargs, action=action)
5339+
for action in ['store', 'append', 'extend']:
5340+
with self.subTest(action=action):
5341+
# nargs=0 is disallowed
5342+
action_name = 'append' if action == 'extend' else action
5343+
self.assertValueError('-x', nargs=0, action=action,
5344+
errmsg=f'nargs for {action_name} actions must be != 0')
5345+
self.assertValueError('spam', nargs=0, action=action,
5346+
errmsg='nargs for positionals must be != 0')
5347+
5348+
# const is disallowed with non-optional arguments
5349+
for nargs in [1, '*', '+']:
5350+
self.assertValueError('-x', const='foo',
5351+
nargs=nargs, action=action)
5352+
self.assertValueError('spam', const='foo',
5353+
nargs=nargs, action=action)
53445354

53455355
def test_required_const_actions(self):
53465356
for action in ['store_const', 'append_const']:
5347-
5348-
# nargs is always disallowed
5349-
self.assertTypeError('-x', nargs='+', action=action)
5357+
with self.subTest(action=action):
5358+
# nargs is always disallowed
5359+
self.assertTypeError('-x', nargs='+', action=action)
53505360

53515361
def test_parsers_action_missing_params(self):
53525362
self.assertTypeError('command', action='parsers')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:meth:`argparse.ArgumentParser.add_argument` now raises exception if
2+
:ref:`action` that does not consume arguments (like 'store_const' or
3+
'strore_true') or explicit ``nargs=0`` are specified for positional
4+
arguments.

0 commit comments

Comments
 (0)