Skip to content

Commit

Permalink
pythongh-58282: Fix support of tuple metavar for positional arguments…
Browse files Browse the repository at this point in the history
… in argparse

Previously, formatting help output or error message for positional argument
with a tuple metavar raised exception.

Co-authored-b: Cyker Way <cykerway@gmail.com>
  • Loading branch information
cykerway authored and serhiy-storchaka committed Sep 30, 2024
1 parent 95581b3 commit b24aa03
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 4 deletions.
13 changes: 10 additions & 3 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,7 @@ def _format_action(self, action):
def _format_action_invocation(self, action):
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
metavar, = self._metavar_formatter(action, default)(1)
return metavar
return ' '.join(self._metavar_formatter(action, default)(1))

else:

Expand Down Expand Up @@ -703,7 +702,15 @@ def _get_action_name(argument):
elif argument.option_strings:
return '/'.join(argument.option_strings)
elif argument.metavar not in (None, SUPPRESS):
return argument.metavar
metavar = argument.metavar
if not isinstance(metavar, tuple):
return metavar
if argument.nargs == ZERO_OR_MORE and len(metavar) == 2:
return '%s[, %s]' % metavar
elif argument.nargs == ONE_OR_MORE:
return '%s[, %s]' % metavar
else:
return ', '.join(metavar)
elif argument.dest not in (None, SUPPRESS):
return argument.dest
elif argument.choices:
Expand Down
98 changes: 97 additions & 1 deletion Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4895,7 +4895,7 @@ class TestHelpNone(HelpTestCase):
version = ''


class TestHelpTupleMetavar(HelpTestCase):
class TestHelpTupleMetavarOptional(HelpTestCase):
"""Test specifying metavar as a tuple"""

parser_signature = Sig(prog='PROG')
Expand All @@ -4922,6 +4922,34 @@ class TestHelpTupleMetavar(HelpTestCase):
version = ''


class TestHelpTupleMetavarPositional(HelpTestCase):
"""Test specifying metavar on a Positional as a tuple"""

parser_signature = Sig(prog='PROG')
argument_signatures = [
Sig('w', help='w help', nargs='+', metavar=('W1', 'W2')),
Sig('x', help='x help', nargs='*', metavar=('X1', 'X2')),
Sig('y', help='y help', nargs=3, metavar=('Y1', 'Y2', 'Y3')),
Sig('z', help='z help', nargs='?', metavar=('Z1',)),
]
argument_group_signatures = []
usage = '''\
usage: PROG [-h] W1 [W2 ...] [X1 [X2 ...]] Y1 Y2 Y3 [Z1]
'''
help = usage + '''\
positional arguments:
W1 W2 w help
X1 X2 x help
Y1 Y2 Y3 y help
Z1 z help
options:
-h, --help show this help message and exit
'''
version = ''


class TestHelpRawText(HelpTestCase):
"""Test the RawTextHelpFormatter"""

Expand Down Expand Up @@ -6518,6 +6546,27 @@ def test_required_args(self):
'the following arguments are required: bar, baz$',
self.parser.parse_args, [])

def test_required_args_with_metavar(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', metavar='BaZ')
self.assertRaisesRegex(argparse.ArgumentError,
'the following arguments are required: bar, BaZ$',
self.parser.parse_args, [])

def test_required_args_n(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs=3)
self.assertRaisesRegex(argparse.ArgumentError,
'the following arguments are required: bar, baz$',
self.parser.parse_args, [])

def test_required_args_n_with_metavar(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs=3, metavar=('B', 'A', 'Z'))
self.assertRaisesRegex(argparse.ArgumentError,
'the following arguments are required: bar, B, A, Z$',
self.parser.parse_args, [])

def test_required_args_optional(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs='?')
Expand All @@ -6532,6 +6581,20 @@ def test_required_args_zero_or_more(self):
'the following arguments are required: bar$',
self.parser.parse_args, [])

def test_required_args_one_or_more(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs='+')
self.assertRaisesRegex(argparse.ArgumentError,
'the following arguments are required: bar, baz$',
self.parser.parse_args, [])

def test_required_args_one_or_more_with_metavar(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs='+', metavar=('BaZ1', 'BaZ2'))
self.assertRaisesRegex(argparse.ArgumentError,
r'the following arguments are required: bar, BaZ1\[, BaZ2]$',
self.parser.parse_args, [])

def test_required_args_remainder(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz', nargs='...')
Expand All @@ -6547,6 +6610,39 @@ def test_required_mutually_exclusive_args(self):
'one of the arguments --bar --baz is required',
self.parser.parse_args, [])

def test_conflicting_mutually_exclusive_args_optional_with_metavar(self):
group = self.parser.add_mutually_exclusive_group()
group.add_argument('--bar')
group.add_argument('baz', nargs='?', metavar='BaZ')
self.assertRaisesRegex(argparse.ArgumentError,
'argument BaZ: not allowed with argument --bar$',
self.parser.parse_args, ['--bar', 'a', 'b'])
self.assertRaisesRegex(argparse.ArgumentError,
'argument --bar: not allowed with argument BaZ$',
self.parser.parse_args, ['a', '--bar', 'b'])

def test_conflicting_mutually_exclusive_args_zero_or_more_with_metavar1(self):
group = self.parser.add_mutually_exclusive_group()
group.add_argument('--bar')
group.add_argument('baz', nargs='*', metavar=('BAZ1',))
self.assertRaisesRegex(argparse.ArgumentError,
'argument BAZ1: not allowed with argument --bar$',
self.parser.parse_args, ['--bar', 'a', 'b'])
self.assertRaisesRegex(argparse.ArgumentError,
'argument --bar: not allowed with argument BAZ1$',
self.parser.parse_args, ['a', '--bar', 'b'])

def test_conflicting_mutually_exclusive_args_zero_or_more_with_metavar2(self):
group = self.parser.add_mutually_exclusive_group()
group.add_argument('--bar')
group.add_argument('baz', nargs='*', metavar=('BAZ1', 'BAZ2'))
self.assertRaisesRegex(argparse.ArgumentError,
r'argument BAZ1\[, BAZ2]: not allowed with argument --bar$',
self.parser.parse_args, ['--bar', 'a', 'b'])
self.assertRaisesRegex(argparse.ArgumentError,
r'argument --bar: not allowed with argument BAZ1\[, BAZ2]$',
self.parser.parse_args, ['a', '--bar', 'b'])

def test_ambiguous_option(self):
self.parser.add_argument('--foobaz')
self.parser.add_argument('--fooble', action='store_true')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`argparse` metavar processing to allow positional arguments to have a
tuple metavar.

0 comments on commit b24aa03

Please sign in to comment.