Skip to content

Commit eb2d268

Browse files
gh-65865: Raise early errors for invalid help strings in argparse (GH-124899)
1 parent 4a943c3 commit eb2d268

File tree

3 files changed

+58
-7
lines changed

3 files changed

+58
-7
lines changed

Lib/argparse.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -588,17 +588,20 @@ def _format_args(self, action, default_metavar):
588588
return result
589589

590590
def _expand_help(self, action):
591+
help_string = self._get_help_string(action)
592+
if '%' not in help_string:
593+
return help_string
591594
params = dict(vars(action), prog=self._prog)
592595
for name in list(params):
593-
if params[name] is SUPPRESS:
596+
value = params[name]
597+
if value is SUPPRESS:
594598
del params[name]
595-
for name in list(params):
596-
if hasattr(params[name], '__name__'):
597-
params[name] = params[name].__name__
599+
elif hasattr(value, '__name__'):
600+
params[name] = value.__name__
598601
if params.get('choices') is not None:
599602
choices_str = ', '.join([str(c) for c in params['choices']])
600603
params['choices'] = choices_str
601-
return self._get_help_string(action) % params
604+
return help_string % params
602605

603606
def _iter_indented_subactions(self, action):
604607
try:
@@ -1180,9 +1183,13 @@ def add_parser(self, name, *, deprecated=False, **kwargs):
11801183
help = kwargs.pop('help')
11811184
choice_action = self._ChoicesPseudoAction(name, aliases, help)
11821185
self._choices_actions.append(choice_action)
1186+
else:
1187+
choice_action = None
11831188

11841189
# create the parser and add it to the map
11851190
parser = self._parser_class(**kwargs)
1191+
if choice_action is not None:
1192+
parser._check_help(choice_action)
11861193
self._name_parser_map[name] = parser
11871194

11881195
# make parser available under aliases also
@@ -1449,11 +1456,12 @@ def add_argument(self, *args, **kwargs):
14491456

14501457
# raise an error if the metavar does not match the type
14511458
if hasattr(self, "_get_formatter"):
1459+
formatter = self._get_formatter()
14521460
try:
1453-
self._get_formatter()._format_args(action, None)
1461+
formatter._format_args(action, None)
14541462
except TypeError:
14551463
raise ValueError("length of metavar tuple does not match nargs")
1456-
1464+
self._check_help(action)
14571465
return self._add_action(action)
14581466

14591467
def add_argument_group(self, *args, **kwargs):
@@ -1635,6 +1643,14 @@ def _handle_conflict_resolve(self, action, conflicting_actions):
16351643
if not action.option_strings:
16361644
action.container._remove_action(action)
16371645

1646+
def _check_help(self, action):
1647+
if action.help and hasattr(self, "_get_formatter"):
1648+
formatter = self._get_formatter()
1649+
try:
1650+
formatter._expand_help(action)
1651+
except (ValueError, TypeError, KeyError) as exc:
1652+
raise ValueError('badly formed help string') from exc
1653+
16381654

16391655
class _ArgumentGroup(_ActionsContainer):
16401656

@@ -1852,6 +1868,7 @@ def add_subparsers(self, **kwargs):
18521868
# create the parsers action and add it to the positionals list
18531869
parsers_class = self._pop_action_class(kwargs, 'parsers')
18541870
action = parsers_class(option_strings=[], **kwargs)
1871+
self._check_help(action)
18551872
self._subparsers._add_action(action)
18561873

18571874
# return the created parsers action

Lib/test/test_argparse.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2623,6 +2623,29 @@ def test_parser_command_help(self):
26232623
--foo foo help
26242624
'''))
26252625

2626+
def assert_bad_help(self, context_type, func, *args, **kwargs):
2627+
with self.assertRaisesRegex(ValueError, 'badly formed help string') as cm:
2628+
func(*args, **kwargs)
2629+
self.assertIsInstance(cm.exception.__context__, context_type)
2630+
2631+
def test_invalid_subparsers_help(self):
2632+
parser = ErrorRaisingArgumentParser(prog='PROG')
2633+
self.assert_bad_help(ValueError, parser.add_subparsers, help='%Y-%m-%d')
2634+
parser = ErrorRaisingArgumentParser(prog='PROG')
2635+
self.assert_bad_help(KeyError, parser.add_subparsers, help='%(spam)s')
2636+
parser = ErrorRaisingArgumentParser(prog='PROG')
2637+
self.assert_bad_help(TypeError, parser.add_subparsers, help='%(prog)d')
2638+
2639+
def test_invalid_subparser_help(self):
2640+
parser = ErrorRaisingArgumentParser(prog='PROG')
2641+
subparsers = parser.add_subparsers()
2642+
self.assert_bad_help(ValueError, subparsers.add_parser, '1',
2643+
help='%Y-%m-%d')
2644+
self.assert_bad_help(KeyError, subparsers.add_parser, '1',
2645+
help='%(spam)s')
2646+
self.assert_bad_help(TypeError, subparsers.add_parser, '1',
2647+
help='%(prog)d')
2648+
26262649
def test_subparser_title_help(self):
26272650
parser = ErrorRaisingArgumentParser(prog='PROG',
26282651
description='main description')
@@ -5375,6 +5398,14 @@ def test_invalid_action(self):
53755398
self.assertValueError('--foo', action="store-true",
53765399
errmsg='unknown action')
53775400

5401+
def test_invalid_help(self):
5402+
self.assertValueError('--foo', help='%Y-%m-%d',
5403+
errmsg='badly formed help string')
5404+
self.assertValueError('--foo', help='%(spam)s',
5405+
errmsg='badly formed help string')
5406+
self.assertValueError('--foo', help='%(prog)d',
5407+
errmsg='badly formed help string')
5408+
53785409
def test_multiple_dest(self):
53795410
parser = argparse.ArgumentParser()
53805411
parser.add_argument(dest='foo')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`argparse` now raises early error for invalid ``help`` arguments to
2+
:meth:`~argparse.ArgumentParser.add_argument`,
3+
:meth:`~argparse.ArgumentParser.add_subparsers` and :meth:`!add_parser`.

0 commit comments

Comments
 (0)