Skip to content

Commit 16127de

Browse files
[3.13] gh-59317: Improve parsing optional positional arguments in argparse (GH-124303) (GH-124436)
Fix parsing positional argument with nargs equal to '?' or '*' if it is preceded by an option and another positional argument. (cherry picked from commit 4a5e4aa) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 03ae82d commit 16127de

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
@@ -2252,18 +2252,19 @@ def _match_argument(self, action, arg_strings_pattern):
22522252
def _match_arguments_partial(self, actions, arg_strings_pattern):
22532253
# progressively shorten the actions list by slicing off the
22542254
# final actions until we find a match
2255-
result = []
22562255
for i in range(len(actions), 0, -1):
22572256
actions_slice = actions[:i]
22582257
pattern = ''.join([self._get_nargs_pattern(action)
22592258
for action in actions_slice])
22602259
match = _re.match(pattern, arg_strings_pattern)
22612260
if match is not None:
2262-
result.extend([len(string) for string in match.groups()])
2263-
break
2264-
2265-
# return the list of arg string counts
2266-
return result
2261+
result = [len(string) for string in match.groups()]
2262+
if (match.end() < len(arg_strings_pattern)
2263+
and arg_strings_pattern[match.end()] == 'O'):
2264+
while result and not result[-1]:
2265+
del result[-1]
2266+
return result
2267+
return []
22672268

22682269
def _parse_optional(self, arg_string):
22692270
# 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
@@ -1132,57 +1134,87 @@ class TestPositionalsNargs2None(ParserTestCase):
11321134
class TestPositionalsNargsNoneZeroOrMore(ParserTestCase):
11331135
"""Test a Positional with no nargs followed by one with unlimited"""
11341136

1135-
argument_signatures = [Sig('foo'), Sig('bar', nargs='*')]
1136-
failures = ['', '--foo']
1137+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='*')]
1138+
failures = ['', '--foo', 'a b -x X c']
11371139
successes = [
1138-
('a', NS(foo='a', bar=[])),
1139-
('a b', NS(foo='a', bar=['b'])),
1140-
('a b c', NS(foo='a', bar=['b', 'c'])),
1140+
('a', NS(x=None, foo='a', bar=[])),
1141+
('a b', NS(x=None, foo='a', bar=['b'])),
1142+
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
1143+
('-x X a', NS(x='X', foo='a', bar=[])),
1144+
('a -x X', NS(x='X', foo='a', bar=[])),
1145+
('-x X a b', NS(x='X', foo='a', bar=['b'])),
1146+
('a -x X b', NS(x='X', foo='a', bar=['b'])),
1147+
('a b -x X', NS(x='X', foo='a', bar=['b'])),
1148+
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
1149+
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
1150+
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
11411151
]
11421152

11431153

11441154
class TestPositionalsNargsNoneOneOrMore(ParserTestCase):
11451155
"""Test a Positional with no nargs followed by one with one or more"""
11461156

1147-
argument_signatures = [Sig('foo'), Sig('bar', nargs='+')]
1148-
failures = ['', '--foo', 'a']
1157+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='+')]
1158+
failures = ['', '--foo', 'a', 'a b -x X c']
11491159
successes = [
1150-
('a b', NS(foo='a', bar=['b'])),
1151-
('a b c', NS(foo='a', bar=['b', 'c'])),
1160+
('a b', NS(x=None, foo='a', bar=['b'])),
1161+
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
1162+
('-x X a b', NS(x='X', foo='a', bar=['b'])),
1163+
('a -x X b', NS(x='X', foo='a', bar=['b'])),
1164+
('a b -x X', NS(x='X', foo='a', bar=['b'])),
1165+
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
1166+
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
1167+
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
11521168
]
11531169

11541170

11551171
class TestPositionalsNargsNoneOptional(ParserTestCase):
11561172
"""Test a Positional with no nargs followed by one with an Optional"""
11571173

1158-
argument_signatures = [Sig('foo'), Sig('bar', nargs='?')]
1174+
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='?')]
11591175
failures = ['', '--foo', 'a b c']
11601176
successes = [
1161-
('a', NS(foo='a', bar=None)),
1162-
('a b', NS(foo='a', bar='b')),
1177+
('a', NS(x=None, foo='a', bar=None)),
1178+
('a b', NS(x=None, foo='a', bar='b')),
1179+
('-x X a', NS(x='X', foo='a', bar=None)),
1180+
('a -x X', NS(x='X', foo='a', bar=None)),
1181+
('-x X a b', NS(x='X', foo='a', bar='b')),
1182+
('a -x X b', NS(x='X', foo='a', bar='b')),
1183+
('a b -x X', NS(x='X', foo='a', bar='b')),
11631184
]
11641185

11651186

11661187
class TestPositionalsNargsZeroOrMoreNone(ParserTestCase):
11671188
"""Test a Positional with unlimited nargs followed by one with none"""
11681189

1169-
argument_signatures = [Sig('foo', nargs='*'), Sig('bar')]
1170-
failures = ['', '--foo']
1190+
argument_signatures = [Sig('-x'), Sig('foo', nargs='*'), Sig('bar')]
1191+
failures = ['', '--foo', 'a -x X b', 'a -x X b c', 'a b -x X c']
11711192
successes = [
1172-
('a', NS(foo=[], bar='a')),
1173-
('a b', NS(foo=['a'], bar='b')),
1174-
('a b c', NS(foo=['a', 'b'], bar='c')),
1193+
('a', NS(x=None, foo=[], bar='a')),
1194+
('a b', NS(x=None, foo=['a'], bar='b')),
1195+
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
1196+
('-x X a', NS(x='X', foo=[], bar='a')),
1197+
('a -x X', NS(x='X', foo=[], bar='a')),
1198+
('-x X a b', NS(x='X', foo=['a'], bar='b')),
1199+
('a b -x X', NS(x='X', foo=['a'], bar='b')),
1200+
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
1201+
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
11751202
]
11761203

11771204

11781205
class TestPositionalsNargsOneOrMoreNone(ParserTestCase):
11791206
"""Test a Positional with one or more nargs followed by one with none"""
11801207

1181-
argument_signatures = [Sig('foo', nargs='+'), Sig('bar')]
1182-
failures = ['', '--foo', 'a']
1208+
argument_signatures = [Sig('-x'), Sig('foo', nargs='+'), Sig('bar')]
1209+
failures = ['', '--foo', 'a', 'a -x X b c', 'a b -x X c']
11831210
successes = [
1184-
('a b', NS(foo=['a'], bar='b')),
1185-
('a b c', NS(foo=['a', 'b'], bar='c')),
1211+
('a b', NS(x=None, foo=['a'], bar='b')),
1212+
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
1213+
('-x X a b', NS(x='X', foo=['a'], bar='b')),
1214+
('a -x X b', NS(x='X', foo=['a'], bar='b')),
1215+
('a b -x X', NS(x='X', foo=['a'], bar='b')),
1216+
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
1217+
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
11861218
]
11871219

11881220

@@ -1267,44 +1299,66 @@ class TestPositionalsNargsNoneZeroOrMore1(ParserTestCase):
12671299
"""Test three Positionals: no nargs, unlimited nargs and 1 nargs"""
12681300

12691301
argument_signatures = [
1302+
Sig('-x'),
12701303
Sig('foo'),
12711304
Sig('bar', nargs='*'),
12721305
Sig('baz', nargs=1),
12731306
]
1274-
failures = ['', '--foo', 'a']
1307+
failures = ['', '--foo', 'a', 'a b -x X c']
12751308
successes = [
1276-
('a b', NS(foo='a', bar=[], baz=['b'])),
1277-
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
1309+
('a b', NS(x=None, foo='a', bar=[], baz=['b'])),
1310+
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
1311+
('-x X a b', NS(x='X', foo='a', bar=[], baz=['b'])),
1312+
('a -x X b', NS(x='X', foo='a', bar=[], baz=['b'])),
1313+
('a b -x X', NS(x='X', foo='a', bar=[], baz=['b'])),
1314+
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1315+
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1316+
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
12781317
]
12791318

12801319

12811320
class TestPositionalsNargsNoneOneOrMore1(ParserTestCase):
12821321
"""Test three Positionals: no nargs, one or more nargs and 1 nargs"""
12831322

12841323
argument_signatures = [
1324+
Sig('-x'),
12851325
Sig('foo'),
12861326
Sig('bar', nargs='+'),
12871327
Sig('baz', nargs=1),
12881328
]
1289-
failures = ['', '--foo', 'a', 'b']
1329+
failures = ['', '--foo', 'a', 'b', 'a b -x X c d', 'a b c -x X d']
12901330
successes = [
1291-
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
1292-
('a b c d', NS(foo='a', bar=['b', 'c'], baz=['d'])),
1331+
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
1332+
('a b c d', NS(x=None, foo='a', bar=['b', 'c'], baz=['d'])),
1333+
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1334+
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1335+
('a b -x X c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1336+
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
1337+
('-x X a b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
1338+
('a -x X b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
1339+
('a b c d -x X', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
12931340
]
12941341

12951342

12961343
class TestPositionalsNargsNoneOptional1(ParserTestCase):
12971344
"""Test three Positionals: no nargs, optional narg and 1 nargs"""
12981345

12991346
argument_signatures = [
1347+
Sig('-x'),
13001348
Sig('foo'),
13011349
Sig('bar', nargs='?', default=0.625),
13021350
Sig('baz', nargs=1),
13031351
]
1304-
failures = ['', '--foo', 'a']
1352+
failures = ['', '--foo', 'a', 'a b -x X c']
13051353
successes = [
1306-
('a b', NS(foo='a', bar=0.625, baz=['b'])),
1307-
('a b c', NS(foo='a', bar='b', baz=['c'])),
1354+
('a b', NS(x=None, foo='a', bar=0.625, baz=['b'])),
1355+
('a b c', NS(x=None, foo='a', bar='b', baz=['c'])),
1356+
('-x X a b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1357+
('a -x X b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1358+
('a b -x X', NS(x='X', foo='a', bar=0.625, baz=['b'])),
1359+
('-x X a b c', NS(x='X', foo='a', bar='b', baz=['c'])),
1360+
('a -x X b c', NS(x='X', foo='a', bar='b', baz=['c'])),
1361+
('a b c -x X', NS(x='X', foo='a', bar='b', baz=['c'])),
13081362
]
13091363

13101364

@@ -1520,6 +1574,9 @@ class TestNargsRemainder(ParserTestCase):
15201574
successes = [
15211575
('X', NS(x='X', y=[], z=None)),
15221576
('-z Z X', NS(x='X', y=[], z='Z')),
1577+
('-z Z X A B', NS(x='X', y=['A', 'B'], z='Z')),
1578+
('X -z Z A B', NS(x='X', y=['-z', 'Z', 'A', 'B'], z=None)),
1579+
('X A -z Z B', NS(x='X', y=['A', '-z', 'Z', 'B'], z=None)),
15231580
('X A B -z Z', NS(x='X', y=['A', 'B', '-z', 'Z'], z=None)),
15241581
('X Y --foo', NS(x='X', y=['Y', '--foo'], z=None)),
15251582
]
@@ -5986,8 +6043,8 @@ def test_basic(self):
59866043

59876044
args, extras = parser.parse_known_args(argv)
59886045
# cannot parse the '1,2,3'
5989-
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[]), args)
5990-
self.assertEqual(["1", "2", "3"], extras)
6046+
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args)
6047+
self.assertEqual(["2", "3"], extras)
59916048

59926049
argv = 'cmd --foo x 1 --error 2 --bar y 3'.split()
59936050
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)