Skip to content

Commit 4a5e4aa

Browse files
gh-59317: Improve parsing optional positional arguments in argparse (GH-124303)
Fix parsing positional argument with nargs equal to '?' or '*' if it is preceded by an option and another positional argument.
1 parent e69ff34 commit 4a5e4aa

File tree

3 files changed

+102
-42
lines changed

3 files changed

+102
-42
lines changed

Lib/argparse.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,18 +2227,19 @@ def _match_argument(self, action, arg_strings_pattern):
22272227
def _match_arguments_partial(self, actions, arg_strings_pattern):
22282228
# progressively shorten the actions list by slicing off the
22292229
# final actions until we find a match
2230-
result = []
22312230
for i in range(len(actions), 0, -1):
22322231
actions_slice = actions[:i]
22332232
pattern = ''.join([self._get_nargs_pattern(action)
22342233
for action in actions_slice])
22352234
match = _re.match(pattern, arg_strings_pattern)
22362235
if match is not None:
2237-
result.extend([len(string) for string in match.groups()])
2238-
break
2239-
2240-
# return the list of arg string counts
2241-
return result
2236+
result = [len(string) for string in match.groups()]
2237+
if (match.end() < len(arg_strings_pattern)
2238+
and arg_strings_pattern[match.end()] == 'O'):
2239+
while result and not result[-1]:
2240+
del result[-1]
2241+
return result
2242+
return []
22422243

22432244
def _parse_optional(self, arg_string):
22442245
# if it's an empty string, it was meant to be a positional

Lib/test/test_argparse.py

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,18 @@ def test_failures(self, tester):
280280
parser = self._get_parser(tester)
281281
for args_str in tester.failures:
282282
args = args_str.split()
283-
with tester.assertRaises(ArgumentParserError, msg=args):
284-
parser.parse_args(args)
283+
with tester.subTest(args=args):
284+
with tester.assertRaises(ArgumentParserError, msg=args):
285+
parser.parse_args(args)
285286

286287
def test_successes(self, tester):
287288
parser = self._get_parser(tester)
288289
for args, expected_ns in tester.successes:
289290
if isinstance(args, str):
290291
args = args.split()
291-
result_ns = self._parse_args(parser, args)
292-
tester.assertEqual(expected_ns, result_ns)
292+
with tester.subTest(args=args):
293+
result_ns = self._parse_args(parser, args)
294+
tester.assertEqual(expected_ns, result_ns)
293295

294296
# add tests for each combination of an optionals adding method
295297
# and an arg parsing method
@@ -1089,57 +1091,87 @@ class TestPositionalsNargs2None(ParserTestCase):
10891091
class TestPositionalsNargsNoneZeroOrMore(ParserTestCase):
10901092
"""Test a Positional with no nargs followed by one with unlimited"""
10911093

1092-
argument_signatures = [Sig('foo'), Sig('bar', nargs='*')]
1093-
failures = ['', '--foo']
1094+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='*')]
1095+
failures = ['', '--foo', 'a b -x X c']
10941096
successes = [
1095-
('a', NS(foo='a', bar=[])),
1096-
('a b', NS(foo='a', bar=['b'])),
1097-
('a b c', NS(foo='a', bar=['b', 'c'])),
1097+
('a', NS(x=None, foo='a', bar=[])),
1098+
('a b', NS(x=None, foo='a', bar=['b'])),
1099+
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
1100+
('-x X a', NS(x='X', foo='a', bar=[])),
1101+
('a -x X', NS(x='X', foo='a', bar=[])),
1102+
('-x X a b', NS(x='X', foo='a', bar=['b'])),
1103+
('a -x X b', NS(x='X', foo='a', bar=['b'])),
1104+
('a b -x X', NS(x='X', foo='a', bar=['b'])),
1105+
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
1106+
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
1107+
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
10981108
]
10991109

11001110

11011111
class TestPositionalsNargsNoneOneOrMore(ParserTestCase):
11021112
"""Test a Positional with no nargs followed by one with one or more"""
11031113

1104-
argument_signatures = [Sig('foo'), Sig('bar', nargs='+')]
1105-
failures = ['', '--foo', 'a']
1114+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='+')]
1115+
failures = ['', '--foo', 'a', 'a b -x X c']
11061116
successes = [
1107-
('a b', NS(foo='a', bar=['b'])),
1108-
('a b c', NS(foo='a', bar=['b', 'c'])),
1117+
('a b', NS(x=None, foo='a', bar=['b'])),
1118+
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
1119+
('-x X a b', NS(x='X', foo='a', bar=['b'])),
1120+
('a -x X b', NS(x='X', foo='a', bar=['b'])),
1121+
('a b -x X', NS(x='X', foo='a', bar=['b'])),
1122+
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
1123+
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
1124+
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
11091125
]
11101126

11111127

11121128
class TestPositionalsNargsNoneOptional(ParserTestCase):
11131129
"""Test a Positional with no nargs followed by one with an Optional"""
11141130

1115-
argument_signatures = [Sig('foo'), Sig('bar', nargs='?')]
1131+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='?')]
11161132
failures = ['', '--foo', 'a b c']
11171133
successes = [
1118-
('a', NS(foo='a', bar=None)),
1119-
('a b', NS(foo='a', bar='b')),
1134+
('a', NS(x=None, foo='a', bar=None)),
1135+
('a b', NS(x=None, foo='a', bar='b')),
1136+
('-x X a', NS(x='X', foo='a', bar=None)),
1137+
('a -x X', NS(x='X', foo='a', bar=None)),
1138+
('-x X a b', NS(x='X', foo='a', bar='b')),
1139+
('a -x X b', NS(x='X', foo='a', bar='b')),
1140+
('a b -x X', NS(x='X', foo='a', bar='b')),
11201141
]
11211142

11221143

11231144
class TestPositionalsNargsZeroOrMoreNone(ParserTestCase):
11241145
"""Test a Positional with unlimited nargs followed by one with none"""
11251146

1126-
argument_signatures = [Sig('foo', nargs='*'), Sig('bar')]
1127-
failures = ['', '--foo']
1147+
argument_signatures = [Sig('-x'), Sig('foo', nargs='*'), Sig('bar')]
1148+
failures = ['', '--foo', 'a -x X b', 'a -x X b c', 'a b -x X c']
11281149
successes = [
1129-
('a', NS(foo=[], bar='a')),
1130-
('a b', NS(foo=['a'], bar='b')),
1131-
('a b c', NS(foo=['a', 'b'], bar='c')),
1150+
('a', NS(x=None, foo=[], bar='a')),
1151+
('a b', NS(x=None, foo=['a'], bar='b')),
1152+
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
1153+
('-x X a', NS(x='X', foo=[], bar='a')),
1154+
('a -x X', NS(x='X', foo=[], bar='a')),
1155+
('-x X a b', NS(x='X', foo=['a'], bar='b')),
1156+
('a b -x X', NS(x='X', foo=['a'], bar='b')),
1157+
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
1158+
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
11321159
]
11331160

11341161

11351162
class TestPositionalsNargsOneOrMoreNone(ParserTestCase):
11361163
"""Test a Positional with one or more nargs followed by one with none"""
11371164

1138-
argument_signatures = [Sig('foo', nargs='+'), Sig('bar')]
1139-
failures = ['', '--foo', 'a']
1165+
argument_signatures = [Sig('-x'), Sig('foo', nargs='+'), Sig('bar')]
1166+
failures = ['', '--foo', 'a', 'a -x X b c', 'a b -x X c']
11401167
successes = [
1141-
('a b', NS(foo=['a'], bar='b')),
1142-
('a b c', NS(foo=['a', 'b'], bar='c')),
1168+
('a b', NS(x=None, foo=['a'], bar='b')),
1169+
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
1170+
('-x X a b', NS(x='X', foo=['a'], bar='b')),
1171+
('a -x X b', NS(x='X', foo=['a'], bar='b')),
1172+
('a b -x X', NS(x='X', foo=['a'], bar='b')),
1173+
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
1174+
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
11431175
]
11441176

11451177

@@ -1224,44 +1256,66 @@ class TestPositionalsNargsNoneZeroOrMore1(ParserTestCase):
12241256
"""Test three Positionals: no nargs, unlimited nargs and 1 nargs"""
12251257

12261258
argument_signatures = [
1259+
Sig('-x'),
12271260
Sig('foo'),
12281261
Sig('bar', nargs='*'),
12291262
Sig('baz', nargs=1),
12301263
]
1231-
failures = ['', '--foo', 'a']
1264+
failures = ['', '--foo', 'a', 'a b -x X c']
12321265
successes = [
1233-
('a b', NS(foo='a', bar=[], baz=['b'])),
1234-
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
1266+
('a b', NS(x=None, foo='a', bar=[], baz=['b'])),
1267+
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
1268+
('-x X a b', NS(x='X', foo='a', bar=[], baz=['b'])),
1269+
('a -x X b', NS(x='X', foo='a', bar=[], baz=['b'])),
1270+
('a b -x X', NS(x='X', foo='a', bar=[], baz=['b'])),
1271+
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1272+
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1273+
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
12351274
]
12361275

12371276

12381277
class TestPositionalsNargsNoneOneOrMore1(ParserTestCase):
12391278
"""Test three Positionals: no nargs, one or more nargs and 1 nargs"""
12401279

12411280
argument_signatures = [
1281+
Sig('-x'),
12421282
Sig('foo'),
12431283
Sig('bar', nargs='+'),
12441284
Sig('baz', nargs=1),
12451285
]
1246-
failures = ['', '--foo', 'a', 'b']
1286+
failures = ['', '--foo', 'a', 'b', 'a b -x X c d', 'a b c -x X d']
12471287
successes = [
1248-
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
1249-
('a b c d', NS(foo='a', bar=['b', 'c'], baz=['d'])),
1288+
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
1289+
('a b c d', NS(x=None, foo='a', bar=['b', 'c'], baz=['d'])),
1290+
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1291+
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1292+
('a b -x X c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1293+
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1294+
('-x X a b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
1295+
('a -x X b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
1296+
('a b c d -x X', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
12501297
]
12511298

12521299

12531300
class TestPositionalsNargsNoneOptional1(ParserTestCase):
12541301
"""Test three Positionals: no nargs, optional narg and 1 nargs"""
12551302

12561303
argument_signatures = [
1304+
Sig('-x'),
12571305
Sig('foo'),
12581306
Sig('bar', nargs='?', default=0.625),
12591307
Sig('baz', nargs=1),
12601308
]
1261-
failures = ['', '--foo', 'a']
1309+
failures = ['', '--foo', 'a', 'a b -x X c']
12621310
successes = [
1263-
('a b', NS(foo='a', bar=0.625, baz=['b'])),
1264-
('a b c', NS(foo='a', bar='b', baz=['c'])),
1311+
('a b', NS(x=None, foo='a', bar=0.625, baz=['b'])),
1312+
('a b c', NS(x=None, foo='a', bar='b', baz=['c'])),
1313+
('-x X a b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1314+
('a -x X b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1315+
('a b -x X', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1316+
('-x X a b c', NS(x='X', foo='a', bar='b', baz=['c'])),
1317+
('a -x X b c', NS(x='X', foo='a', bar='b', baz=['c'])),
1318+
('a b c -x X', NS(x='X', foo='a', bar='b', baz=['c'])),
12651319
]
12661320

12671321

@@ -1477,6 +1531,9 @@ class TestNargsRemainder(ParserTestCase):
14771531
successes = [
14781532
('X', NS(x='X', y=[], z=None)),
14791533
('-z Z X', NS(x='X', y=[], z='Z')),
1534+
('-z Z X A B', NS(x='X', y=['A', 'B'], z='Z')),
1535+
('X -z Z A B', NS(x='X', y=['-z', 'Z', 'A', 'B'], z=None)),
1536+
('X A -z Z B', NS(x='X', y=['A', '-z', 'Z', 'B'], z=None)),
14801537
('X A B -z Z', NS(x='X', y=['A', 'B', '-z', 'Z'], z=None)),
14811538
('X Y --foo', NS(x='X', y=['Y', '--foo'], z=None)),
14821539
]
@@ -6018,8 +6075,8 @@ def test_basic(self):
60186075

60196076
args, extras = parser.parse_known_args(argv)
60206077
# cannot parse the '1,2,3'
6021-
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[]), args)
6022-
self.assertEqual(["1", "2", "3"], extras)
6078+
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args)
6079+
self.assertEqual(["2", "3"], extras)
60236080

60246081
argv = 'cmd --foo x 1 --error 2 --bar y 3'.split()
60256082
args, extras = parser.parse_known_intermixed_args(argv)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix parsing positional argument with :ref:`nargs` equal to ``'?'`` or ``'*'``
2+
if it is preceded by an option and another positional argument.

0 commit comments

Comments
 (0)