mirror of
https://github.com/python/cpython.git
synced 2025-08-09 11:29:45 +00:00
[3.12] gh-59317: Improve parsing optional positional arguments in argparse (GH-124303) (GH-124437)
Fix parsing positional argument with nargs equal to '?' or '*' if it is
preceded by an option and another positional argument.
(cherry picked from commit 4a5e4aade4
)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
ed44377220
commit
996e409401
3 changed files with 102 additions and 42 deletions
|
@ -2260,18 +2260,19 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
def _match_arguments_partial(self, actions, arg_strings_pattern):
|
def _match_arguments_partial(self, actions, arg_strings_pattern):
|
||||||
# progressively shorten the actions list by slicing off the
|
# progressively shorten the actions list by slicing off the
|
||||||
# final actions until we find a match
|
# final actions until we find a match
|
||||||
result = []
|
|
||||||
for i in range(len(actions), 0, -1):
|
for i in range(len(actions), 0, -1):
|
||||||
actions_slice = actions[:i]
|
actions_slice = actions[:i]
|
||||||
pattern = ''.join([self._get_nargs_pattern(action)
|
pattern = ''.join([self._get_nargs_pattern(action)
|
||||||
for action in actions_slice])
|
for action in actions_slice])
|
||||||
match = _re.match(pattern, arg_strings_pattern)
|
match = _re.match(pattern, arg_strings_pattern)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
result.extend([len(string) for string in match.groups()])
|
result = [len(string) for string in match.groups()]
|
||||||
break
|
if (match.end() < len(arg_strings_pattern)
|
||||||
|
and arg_strings_pattern[match.end()] == 'O'):
|
||||||
# return the list of arg string counts
|
while result and not result[-1]:
|
||||||
return result
|
del result[-1]
|
||||||
|
return result
|
||||||
|
return []
|
||||||
|
|
||||||
def _parse_optional(self, arg_string):
|
def _parse_optional(self, arg_string):
|
||||||
# if it's an empty string, it was meant to be a positional
|
# if it's an empty string, it was meant to be a positional
|
||||||
|
|
|
@ -280,16 +280,18 @@ class ParserTesterMetaclass(type):
|
||||||
parser = self._get_parser(tester)
|
parser = self._get_parser(tester)
|
||||||
for args_str in tester.failures:
|
for args_str in tester.failures:
|
||||||
args = args_str.split()
|
args = args_str.split()
|
||||||
with tester.assertRaises(ArgumentParserError, msg=args):
|
with tester.subTest(args=args):
|
||||||
parser.parse_args(args)
|
with tester.assertRaises(ArgumentParserError, msg=args):
|
||||||
|
parser.parse_args(args)
|
||||||
|
|
||||||
def test_successes(self, tester):
|
def test_successes(self, tester):
|
||||||
parser = self._get_parser(tester)
|
parser = self._get_parser(tester)
|
||||||
for args, expected_ns in tester.successes:
|
for args, expected_ns in tester.successes:
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = args.split()
|
args = args.split()
|
||||||
result_ns = self._parse_args(parser, args)
|
with tester.subTest(args=args):
|
||||||
tester.assertEqual(expected_ns, result_ns)
|
result_ns = self._parse_args(parser, args)
|
||||||
|
tester.assertEqual(expected_ns, result_ns)
|
||||||
|
|
||||||
# add tests for each combination of an optionals adding method
|
# add tests for each combination of an optionals adding method
|
||||||
# and an arg parsing method
|
# and an arg parsing method
|
||||||
|
@ -1132,57 +1134,87 @@ class TestPositionalsNargs2None(ParserTestCase):
|
||||||
class TestPositionalsNargsNoneZeroOrMore(ParserTestCase):
|
class TestPositionalsNargsNoneZeroOrMore(ParserTestCase):
|
||||||
"""Test a Positional with no nargs followed by one with unlimited"""
|
"""Test a Positional with no nargs followed by one with unlimited"""
|
||||||
|
|
||||||
argument_signatures = [Sig('foo'), Sig('bar', nargs='*')]
|
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='*')]
|
||||||
failures = ['', '--foo']
|
failures = ['', '--foo', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a', NS(foo='a', bar=[])),
|
('a', NS(x=None, foo='a', bar=[])),
|
||||||
('a b', NS(foo='a', bar=['b'])),
|
('a b', NS(x=None, foo='a', bar=['b'])),
|
||||||
('a b c', NS(foo='a', bar=['b', 'c'])),
|
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
|
||||||
|
('-x X a', NS(x='X', foo='a', bar=[])),
|
||||||
|
('a -x X', NS(x='X', foo='a', bar=[])),
|
||||||
|
('-x X a b', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('a -x X b', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('a b -x X', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
|
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
|
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPositionalsNargsNoneOneOrMore(ParserTestCase):
|
class TestPositionalsNargsNoneOneOrMore(ParserTestCase):
|
||||||
"""Test a Positional with no nargs followed by one with one or more"""
|
"""Test a Positional with no nargs followed by one with one or more"""
|
||||||
|
|
||||||
argument_signatures = [Sig('foo'), Sig('bar', nargs='+')]
|
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='+')]
|
||||||
failures = ['', '--foo', 'a']
|
failures = ['', '--foo', 'a', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a b', NS(foo='a', bar=['b'])),
|
('a b', NS(x=None, foo='a', bar=['b'])),
|
||||||
('a b c', NS(foo='a', bar=['b', 'c'])),
|
('a b c', NS(x=None, foo='a', bar=['b', 'c'])),
|
||||||
|
('-x X a b', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('a -x X b', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('a b -x X', NS(x='X', foo='a', bar=['b'])),
|
||||||
|
('-x X a b c', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
|
('a -x X b c', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
|
('a b c -x X', NS(x='X', foo='a', bar=['b', 'c'])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPositionalsNargsNoneOptional(ParserTestCase):
|
class TestPositionalsNargsNoneOptional(ParserTestCase):
|
||||||
"""Test a Positional with no nargs followed by one with an Optional"""
|
"""Test a Positional with no nargs followed by one with an Optional"""
|
||||||
|
|
||||||
argument_signatures = [Sig('foo'), Sig('bar', nargs='?')]
|
argument_signatures = [Sig('-x'), Sig('foo'), Sig('bar', nargs='?')]
|
||||||
failures = ['', '--foo', 'a b c']
|
failures = ['', '--foo', 'a b c']
|
||||||
successes = [
|
successes = [
|
||||||
('a', NS(foo='a', bar=None)),
|
('a', NS(x=None, foo='a', bar=None)),
|
||||||
('a b', NS(foo='a', bar='b')),
|
('a b', NS(x=None, foo='a', bar='b')),
|
||||||
|
('-x X a', NS(x='X', foo='a', bar=None)),
|
||||||
|
('a -x X', NS(x='X', foo='a', bar=None)),
|
||||||
|
('-x X a b', NS(x='X', foo='a', bar='b')),
|
||||||
|
('a -x X b', NS(x='X', foo='a', bar='b')),
|
||||||
|
('a b -x X', NS(x='X', foo='a', bar='b')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPositionalsNargsZeroOrMoreNone(ParserTestCase):
|
class TestPositionalsNargsZeroOrMoreNone(ParserTestCase):
|
||||||
"""Test a Positional with unlimited nargs followed by one with none"""
|
"""Test a Positional with unlimited nargs followed by one with none"""
|
||||||
|
|
||||||
argument_signatures = [Sig('foo', nargs='*'), Sig('bar')]
|
argument_signatures = [Sig('-x'), Sig('foo', nargs='*'), Sig('bar')]
|
||||||
failures = ['', '--foo']
|
failures = ['', '--foo', 'a -x X b', 'a -x X b c', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a', NS(foo=[], bar='a')),
|
('a', NS(x=None, foo=[], bar='a')),
|
||||||
('a b', NS(foo=['a'], bar='b')),
|
('a b', NS(x=None, foo=['a'], bar='b')),
|
||||||
('a b c', NS(foo=['a', 'b'], bar='c')),
|
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
|
||||||
|
('-x X a', NS(x='X', foo=[], bar='a')),
|
||||||
|
('a -x X', NS(x='X', foo=[], bar='a')),
|
||||||
|
('-x X a b', NS(x='X', foo=['a'], bar='b')),
|
||||||
|
('a b -x X', NS(x='X', foo=['a'], bar='b')),
|
||||||
|
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
|
||||||
|
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestPositionalsNargsOneOrMoreNone(ParserTestCase):
|
class TestPositionalsNargsOneOrMoreNone(ParserTestCase):
|
||||||
"""Test a Positional with one or more nargs followed by one with none"""
|
"""Test a Positional with one or more nargs followed by one with none"""
|
||||||
|
|
||||||
argument_signatures = [Sig('foo', nargs='+'), Sig('bar')]
|
argument_signatures = [Sig('-x'), Sig('foo', nargs='+'), Sig('bar')]
|
||||||
failures = ['', '--foo', 'a']
|
failures = ['', '--foo', 'a', 'a -x X b c', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a b', NS(foo=['a'], bar='b')),
|
('a b', NS(x=None, foo=['a'], bar='b')),
|
||||||
('a b c', NS(foo=['a', 'b'], bar='c')),
|
('a b c', NS(x=None, foo=['a', 'b'], bar='c')),
|
||||||
|
('-x X a b', NS(x='X', foo=['a'], bar='b')),
|
||||||
|
('a -x X b', NS(x='X', foo=['a'], bar='b')),
|
||||||
|
('a b -x X', NS(x='X', foo=['a'], bar='b')),
|
||||||
|
('-x X a b c', NS(x='X', foo=['a', 'b'], bar='c')),
|
||||||
|
('a b c -x X', NS(x='X', foo=['a', 'b'], bar='c')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1267,14 +1299,21 @@ class TestPositionalsNargsNoneZeroOrMore1(ParserTestCase):
|
||||||
"""Test three Positionals: no nargs, unlimited nargs and 1 nargs"""
|
"""Test three Positionals: no nargs, unlimited nargs and 1 nargs"""
|
||||||
|
|
||||||
argument_signatures = [
|
argument_signatures = [
|
||||||
|
Sig('-x'),
|
||||||
Sig('foo'),
|
Sig('foo'),
|
||||||
Sig('bar', nargs='*'),
|
Sig('bar', nargs='*'),
|
||||||
Sig('baz', nargs=1),
|
Sig('baz', nargs=1),
|
||||||
]
|
]
|
||||||
failures = ['', '--foo', 'a']
|
failures = ['', '--foo', 'a', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a b', NS(foo='a', bar=[], baz=['b'])),
|
('a b', NS(x=None, foo='a', bar=[], baz=['b'])),
|
||||||
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
|
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('-x X a b', NS(x='X', foo='a', bar=[], baz=['b'])),
|
||||||
|
('a -x X b', NS(x='X', foo='a', bar=[], baz=['b'])),
|
||||||
|
('a b -x X', NS(x='X', foo='a', bar=[], baz=['b'])),
|
||||||
|
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1282,14 +1321,22 @@ class TestPositionalsNargsNoneOneOrMore1(ParserTestCase):
|
||||||
"""Test three Positionals: no nargs, one or more nargs and 1 nargs"""
|
"""Test three Positionals: no nargs, one or more nargs and 1 nargs"""
|
||||||
|
|
||||||
argument_signatures = [
|
argument_signatures = [
|
||||||
|
Sig('-x'),
|
||||||
Sig('foo'),
|
Sig('foo'),
|
||||||
Sig('bar', nargs='+'),
|
Sig('bar', nargs='+'),
|
||||||
Sig('baz', nargs=1),
|
Sig('baz', nargs=1),
|
||||||
]
|
]
|
||||||
failures = ['', '--foo', 'a', 'b']
|
failures = ['', '--foo', 'a', 'b', 'a b -x X c d', 'a b c -x X d']
|
||||||
successes = [
|
successes = [
|
||||||
('a b c', NS(foo='a', bar=['b'], baz=['c'])),
|
('a b c', NS(x=None, foo='a', bar=['b'], baz=['c'])),
|
||||||
('a b c d', NS(foo='a', bar=['b', 'c'], baz=['d'])),
|
('a b c d', NS(x=None, foo='a', bar=['b', 'c'], baz=['d'])),
|
||||||
|
('-x X a b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('a -x X b c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('a b -x X c', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('a b c -x X', NS(x='X', foo='a', bar=['b'], baz=['c'])),
|
||||||
|
('-x X a b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
|
||||||
|
('a -x X b c d', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
|
||||||
|
('a b c d -x X', NS(x='X', foo='a', bar=['b', 'c'], baz=['d'])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1297,14 +1344,21 @@ class TestPositionalsNargsNoneOptional1(ParserTestCase):
|
||||||
"""Test three Positionals: no nargs, optional narg and 1 nargs"""
|
"""Test three Positionals: no nargs, optional narg and 1 nargs"""
|
||||||
|
|
||||||
argument_signatures = [
|
argument_signatures = [
|
||||||
|
Sig('-x'),
|
||||||
Sig('foo'),
|
Sig('foo'),
|
||||||
Sig('bar', nargs='?', default=0.625),
|
Sig('bar', nargs='?', default=0.625),
|
||||||
Sig('baz', nargs=1),
|
Sig('baz', nargs=1),
|
||||||
]
|
]
|
||||||
failures = ['', '--foo', 'a']
|
failures = ['', '--foo', 'a', 'a b -x X c']
|
||||||
successes = [
|
successes = [
|
||||||
('a b', NS(foo='a', bar=0.625, baz=['b'])),
|
('a b', NS(x=None, foo='a', bar=0.625, baz=['b'])),
|
||||||
('a b c', NS(foo='a', bar='b', baz=['c'])),
|
('a b c', NS(x=None, foo='a', bar='b', baz=['c'])),
|
||||||
|
('-x X a b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
|
||||||
|
('a -x X b', NS(x='X', foo='a', bar=0.625, baz=['b'])),
|
||||||
|
('a b -x X', NS(x='X', foo='a', bar=0.625, baz=['b'])),
|
||||||
|
('-x X a b c', NS(x='X', foo='a', bar='b', baz=['c'])),
|
||||||
|
('a -x X b c', NS(x='X', foo='a', bar='b', baz=['c'])),
|
||||||
|
('a b c -x X', NS(x='X', foo='a', bar='b', baz=['c'])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1481,6 +1535,9 @@ class TestNargsRemainder(ParserTestCase):
|
||||||
successes = [
|
successes = [
|
||||||
('X', NS(x='X', y=[], z=None)),
|
('X', NS(x='X', y=[], z=None)),
|
||||||
('-z Z X', NS(x='X', y=[], z='Z')),
|
('-z Z X', NS(x='X', y=[], z='Z')),
|
||||||
|
('-z Z X A B', NS(x='X', y=['A', 'B'], z='Z')),
|
||||||
|
('X -z Z A B', NS(x='X', y=['-z', 'Z', 'A', 'B'], z=None)),
|
||||||
|
('X A -z Z B', NS(x='X', y=['A', '-z', 'Z', 'B'], z=None)),
|
||||||
('X A B -z Z', NS(x='X', y=['A', 'B', '-z', 'Z'], z=None)),
|
('X A B -z Z', NS(x='X', y=['A', 'B', '-z', 'Z'], z=None)),
|
||||||
('X Y --foo', NS(x='X', y=['Y', '--foo'], z=None)),
|
('X Y --foo', NS(x='X', y=['Y', '--foo'], z=None)),
|
||||||
]
|
]
|
||||||
|
@ -5679,8 +5736,8 @@ class TestIntermixedArgs(TestCase):
|
||||||
|
|
||||||
args, extras = parser.parse_known_args(argv)
|
args, extras = parser.parse_known_args(argv)
|
||||||
# cannot parse the '1,2,3'
|
# cannot parse the '1,2,3'
|
||||||
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[]), args)
|
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args)
|
||||||
self.assertEqual(["1", "2", "3"], extras)
|
self.assertEqual(["2", "3"], extras)
|
||||||
|
|
||||||
argv = 'cmd --foo x 1 --error 2 --bar y 3'.split()
|
argv = 'cmd --foo x 1 --error 2 --bar y 3'.split()
|
||||||
args, extras = parser.parse_known_intermixed_args(argv)
|
args, extras = parser.parse_known_intermixed_args(argv)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix parsing positional argument with :ref:`nargs` equal to ``'?'`` or ``'*'``
|
||||||
|
if it is preceded by an option and another positional argument.
|
Loading…
Add table
Add a link
Reference in a new issue