Skip to content

Commit e28f2c6

Browse files
[3.13] gh-58282: Fix support of tuple metavar for positional arguments in argparse (GH-124782) (GH-124882)
Previously, formatting help output or error message for positional argument with a tuple metavar raised exception. (cherry picked from commit 9b31a2d) Co-authored-by: Cyker Way <[email protected]>
1 parent 1e820e6 commit e28f2c6

File tree

3 files changed

+109
-4
lines changed

3 files changed

+109
-4
lines changed

Lib/argparse.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,7 @@ def _format_action(self, action):
527527
def _format_action_invocation(self, action):
528528
if not action.option_strings:
529529
default = self._get_default_metavar_for_positional(action)
530-
metavar, = self._metavar_formatter(action, default)(1)
531-
return metavar
530+
return ' '.join(self._metavar_formatter(action, default)(1))
532531

533532
else:
534533

@@ -703,7 +702,15 @@ def _get_action_name(argument):
703702
elif argument.option_strings:
704703
return '/'.join(argument.option_strings)
705704
elif argument.metavar not in (None, SUPPRESS):
706-
return argument.metavar
705+
metavar = argument.metavar
706+
if not isinstance(metavar, tuple):
707+
return metavar
708+
if argument.nargs == ZERO_OR_MORE and len(metavar) == 2:
709+
return '%s[, %s]' % metavar
710+
elif argument.nargs == ONE_OR_MORE:
711+
return '%s[, %s]' % metavar
712+
else:
713+
return ', '.join(metavar)
707714
elif argument.dest not in (None, SUPPRESS):
708715
return argument.dest
709716
elif argument.choices:

Lib/test/test_argparse.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4893,7 +4893,7 @@ class TestHelpNone(HelpTestCase):
48934893
version = ''
48944894

48954895

4896-
class TestHelpTupleMetavar(HelpTestCase):
4896+
class TestHelpTupleMetavarOptional(HelpTestCase):
48974897
"""Test specifying metavar as a tuple"""
48984898

48994899
parser_signature = Sig(prog='PROG')
@@ -4920,6 +4920,34 @@ class TestHelpTupleMetavar(HelpTestCase):
49204920
version = ''
49214921

49224922

4923+
class TestHelpTupleMetavarPositional(HelpTestCase):
4924+
"""Test specifying metavar on a Positional as a tuple"""
4925+
4926+
parser_signature = Sig(prog='PROG')
4927+
argument_signatures = [
4928+
Sig('w', help='w help', nargs='+', metavar=('W1', 'W2')),
4929+
Sig('x', help='x help', nargs='*', metavar=('X1', 'X2')),
4930+
Sig('y', help='y help', nargs=3, metavar=('Y1', 'Y2', 'Y3')),
4931+
Sig('z', help='z help', nargs='?', metavar=('Z1',)),
4932+
]
4933+
argument_group_signatures = []
4934+
usage = '''\
4935+
usage: PROG [-h] W1 [W2 ...] [X1 [X2 ...]] Y1 Y2 Y3 [Z1]
4936+
'''
4937+
help = usage + '''\
4938+
4939+
positional arguments:
4940+
W1 W2 w help
4941+
X1 X2 x help
4942+
Y1 Y2 Y3 y help
4943+
Z1 z help
4944+
4945+
options:
4946+
-h, --help show this help message and exit
4947+
'''
4948+
version = ''
4949+
4950+
49234951
class TestHelpRawText(HelpTestCase):
49244952
"""Test the RawTextHelpFormatter"""
49254953

@@ -6516,6 +6544,27 @@ def test_required_args(self):
65166544
'the following arguments are required: bar, baz$',
65176545
self.parser.parse_args, [])
65186546

6547+
def test_required_args_with_metavar(self):
6548+
self.parser.add_argument('bar')
6549+
self.parser.add_argument('baz', metavar='BaZ')
6550+
self.assertRaisesRegex(argparse.ArgumentError,
6551+
'the following arguments are required: bar, BaZ$',
6552+
self.parser.parse_args, [])
6553+
6554+
def test_required_args_n(self):
6555+
self.parser.add_argument('bar')
6556+
self.parser.add_argument('baz', nargs=3)
6557+
self.assertRaisesRegex(argparse.ArgumentError,
6558+
'the following arguments are required: bar, baz$',
6559+
self.parser.parse_args, [])
6560+
6561+
def test_required_args_n_with_metavar(self):
6562+
self.parser.add_argument('bar')
6563+
self.parser.add_argument('baz', nargs=3, metavar=('B', 'A', 'Z'))
6564+
self.assertRaisesRegex(argparse.ArgumentError,
6565+
'the following arguments are required: bar, B, A, Z$',
6566+
self.parser.parse_args, [])
6567+
65196568
def test_required_args_optional(self):
65206569
self.parser.add_argument('bar')
65216570
self.parser.add_argument('baz', nargs='?')
@@ -6530,6 +6579,20 @@ def test_required_args_zero_or_more(self):
65306579
'the following arguments are required: bar$',
65316580
self.parser.parse_args, [])
65326581

6582+
def test_required_args_one_or_more(self):
6583+
self.parser.add_argument('bar')
6584+
self.parser.add_argument('baz', nargs='+')
6585+
self.assertRaisesRegex(argparse.ArgumentError,
6586+
'the following arguments are required: bar, baz$',
6587+
self.parser.parse_args, [])
6588+
6589+
def test_required_args_one_or_more_with_metavar(self):
6590+
self.parser.add_argument('bar')
6591+
self.parser.add_argument('baz', nargs='+', metavar=('BaZ1', 'BaZ2'))
6592+
self.assertRaisesRegex(argparse.ArgumentError,
6593+
r'the following arguments are required: bar, BaZ1\[, BaZ2]$',
6594+
self.parser.parse_args, [])
6595+
65336596
def test_required_args_remainder(self):
65346597
self.parser.add_argument('bar')
65356598
self.parser.add_argument('baz', nargs='...')
@@ -6545,6 +6608,39 @@ def test_required_mutually_exclusive_args(self):
65456608
'one of the arguments --bar --baz is required',
65466609
self.parser.parse_args, [])
65476610

6611+
def test_conflicting_mutually_exclusive_args_optional_with_metavar(self):
6612+
group = self.parser.add_mutually_exclusive_group()
6613+
group.add_argument('--bar')
6614+
group.add_argument('baz', nargs='?', metavar='BaZ')
6615+
self.assertRaisesRegex(argparse.ArgumentError,
6616+
'argument BaZ: not allowed with argument --bar$',
6617+
self.parser.parse_args, ['--bar', 'a', 'b'])
6618+
self.assertRaisesRegex(argparse.ArgumentError,
6619+
'argument --bar: not allowed with argument BaZ$',
6620+
self.parser.parse_args, ['a', '--bar', 'b'])
6621+
6622+
def test_conflicting_mutually_exclusive_args_zero_or_more_with_metavar1(self):
6623+
group = self.parser.add_mutually_exclusive_group()
6624+
group.add_argument('--bar')
6625+
group.add_argument('baz', nargs='*', metavar=('BAZ1',))
6626+
self.assertRaisesRegex(argparse.ArgumentError,
6627+
'argument BAZ1: not allowed with argument --bar$',
6628+
self.parser.parse_args, ['--bar', 'a', 'b'])
6629+
self.assertRaisesRegex(argparse.ArgumentError,
6630+
'argument --bar: not allowed with argument BAZ1$',
6631+
self.parser.parse_args, ['a', '--bar', 'b'])
6632+
6633+
def test_conflicting_mutually_exclusive_args_zero_or_more_with_metavar2(self):
6634+
group = self.parser.add_mutually_exclusive_group()
6635+
group.add_argument('--bar')
6636+
group.add_argument('baz', nargs='*', metavar=('BAZ1', 'BAZ2'))
6637+
self.assertRaisesRegex(argparse.ArgumentError,
6638+
r'argument BAZ1\[, BAZ2]: not allowed with argument --bar$',
6639+
self.parser.parse_args, ['--bar', 'a', 'b'])
6640+
self.assertRaisesRegex(argparse.ArgumentError,
6641+
r'argument --bar: not allowed with argument BAZ1\[, BAZ2]$',
6642+
self.parser.parse_args, ['a', '--bar', 'b'])
6643+
65486644
def test_ambiguous_option(self):
65496645
self.parser.add_argument('--foobaz')
65506646
self.parser.add_argument('--fooble', action='store_true')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`argparse` metavar processing to allow positional arguments to have a
2+
tuple metavar.

0 commit comments

Comments
 (0)