[3.13] gh-125355: Rewrite parse_intermixed_args() in argparse (GH-125356) (GH-125834)

* The parser no longer changes temporarily during parsing.
* Default values are not processed twice.
* Required mutually exclusive groups containing positional arguments are
  now supported.
* The missing arguments report now includes the names of all required
  optional and positional arguments.
* Unknown options can be intermixed with positional arguments in
  parse_known_intermixed_args().
(cherry picked from commit 759a54d28f)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Miss Islington (bot) 2024-10-22 14:58:05 +02:00 committed by GitHub
parent e3bfe1e756
commit 1fe63b15eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 82 deletions

View file

@ -6257,12 +6257,23 @@ class TestIntermixedArgs(TestCase):
# cannot parse the '1,2,3'
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args)
self.assertEqual(["2", "3"], extras)
args, extras = parser.parse_known_intermixed_args(argv)
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]), args)
self.assertEqual([], extras)
# unknown optionals go into extras
argv = 'cmd --foo x --error 1 2 --bar y 3'.split()
args, extras = parser.parse_known_intermixed_args(argv)
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]), args)
self.assertEqual(['--error'], extras)
argv = 'cmd --foo x 1 --error 2 --bar y 3'.split()
args, extras = parser.parse_known_intermixed_args(argv)
# unknown optionals go into extras
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1]), args)
self.assertEqual(['--error', '2', '3'], extras)
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]), args)
self.assertEqual(['--error'], extras)
argv = 'cmd --foo x 1 2 --error --bar y 3'.split()
args, extras = parser.parse_known_intermixed_args(argv)
self.assertEqual(NS(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]), args)
self.assertEqual(['--error'], extras)
# restores attributes that were temporarily changed
self.assertIsNone(parser.usage)
@ -6281,37 +6292,48 @@ class TestIntermixedArgs(TestCase):
parser.parse_intermixed_args(argv)
self.assertRegex(str(cm.exception), r'\.\.\.')
def test_exclusive(self):
# mutually exclusive group; intermixed works fine
parser = ErrorRaisingArgumentParser(prog='PROG')
def test_required_exclusive(self):
# required mutually exclusive group; intermixed works fine
parser = argparse.ArgumentParser(prog='PROG', exit_on_error=False)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--foo', action='store_true', help='FOO')
group.add_argument('--spam', help='SPAM')
parser.add_argument('badger', nargs='*', default='X', help='BADGER')
args = parser.parse_intermixed_args('--foo 1 2'.split())
self.assertEqual(NS(badger=['1', '2'], foo=True, spam=None), args)
args = parser.parse_intermixed_args('1 --foo 2'.split())
self.assertEqual(NS(badger=['1', '2'], foo=True, spam=None), args)
self.assertRaises(ArgumentParserError, parser.parse_intermixed_args, '1 2'.split())
self.assertRaisesRegex(argparse.ArgumentError,
'one of the arguments --foo --spam is required',
parser.parse_intermixed_args, '1 2'.split())
self.assertEqual(group.required, True)
def test_exclusive_incompatible(self):
# mutually exclusive group including positional - fail
parser = ErrorRaisingArgumentParser(prog='PROG')
def test_required_exclusive_with_positional(self):
# required mutually exclusive group with positional argument
parser = argparse.ArgumentParser(prog='PROG', exit_on_error=False)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--foo', action='store_true', help='FOO')
group.add_argument('--spam', help='SPAM')
group.add_argument('badger', nargs='*', default='X', help='BADGER')
self.assertRaises(TypeError, parser.parse_intermixed_args, [])
args = parser.parse_intermixed_args(['--foo'])
self.assertEqual(NS(foo=True, spam=None, badger='X'), args)
args = parser.parse_intermixed_args(['a', 'b'])
self.assertEqual(NS(foo=False, spam=None, badger=['a', 'b']), args)
self.assertRaisesRegex(argparse.ArgumentError,
'one of the arguments --foo --spam badger is required',
parser.parse_intermixed_args, [])
self.assertRaisesRegex(argparse.ArgumentError,
'argument badger: not allowed with argument --foo',
parser.parse_intermixed_args, ['--foo', 'a', 'b'])
self.assertRaisesRegex(argparse.ArgumentError,
'argument badger: not allowed with argument --foo',
parser.parse_intermixed_args, ['a', '--foo', 'b'])
self.assertEqual(group.required, True)
def test_invalid_args(self):
parser = ErrorRaisingArgumentParser(prog='PROG')
self.assertRaises(ArgumentParserError, parser.parse_intermixed_args, ['a'])
parser = ErrorRaisingArgumentParser(prog='PROG')
parser.add_argument('--foo', nargs="*")
parser.add_argument('foo')
with self.assertWarns(UserWarning):
parser.parse_intermixed_args(['hello', '--foo'])
class TestIntermixedMessageContentError(TestCase):
# case where Intermixed gives different error message
@ -6330,7 +6352,7 @@ class TestIntermixedMessageContentError(TestCase):
with self.assertRaises(ArgumentParserError) as cm:
parser.parse_intermixed_args([])
msg = str(cm.exception)
self.assertNotRegex(msg, 'req_pos')
self.assertRegex(msg, 'req_pos')
self.assertRegex(msg, 'req_opt')
# ==========================