Skip to content

Commit 9a3263f

Browse files
GH-142950: Process format specifiers before colourization in argparse help (#142960)
1 parent d043949 commit 9a3263f

File tree

3 files changed

+67
-4
lines changed

3 files changed

+67
-4
lines changed

Lib/argparse.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,11 +688,41 @@ def _expand_help(self, action):
688688
params[name] = value.__name__
689689
if params.get('choices') is not None:
690690
params['choices'] = ', '.join(map(str, params['choices']))
691-
# Before interpolating, wrap the values with color codes
691+
692692
t = self._theme
693-
for name, value in params.items():
694-
params[name] = f"{t.interpolated_value}{value}{t.reset}"
695-
return help_string % params
693+
694+
result = help_string % params
695+
696+
if not t.reset:
697+
return result
698+
699+
# Match format specifiers like: %s, %d, %(key)s, etc.
700+
fmt_spec = r'''
701+
%
702+
(?:
703+
% # %% escape
704+
|
705+
(?:\((?P<key>[^)]*)\))? # key
706+
[-#0\ +]* # flags
707+
(?:\*|\d+)? # width
708+
(?:\.(?:\*|\d+))? # precision
709+
[hlL]? # length modifier
710+
[diouxXeEfFgGcrsa] # conversion type
711+
)
712+
'''
713+
714+
def colorize(match):
715+
spec, key = match.group(0, 'key')
716+
if spec == '%%':
717+
return '%'
718+
if key is not None:
719+
# %(key)... - format and colorize
720+
formatted = spec % {key: params[key]}
721+
return f'{t.interpolated_value}{formatted}{t.reset}'
722+
# bare %s etc. - format with full params dict, no colorization
723+
return spec % params
724+
725+
return _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE)
696726

697727
def _iter_indented_subactions(self, action):
698728
try:

Lib/test/test_argparse.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7663,6 +7663,38 @@ def test_backtick_markup_special_regex_chars(self):
76637663
help_text = parser.format_help()
76647664
self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text)
76657665

7666+
def test_help_with_format_specifiers(self):
7667+
# GH-142950: format specifiers like %x should work with color=True
7668+
parser = argparse.ArgumentParser(prog='PROG', color=True)
7669+
parser.add_argument('--hex', type=int, default=255,
7670+
help='hex: %(default)x, alt: %(default)#x')
7671+
parser.add_argument('--zero', type=int, default=7,
7672+
help='zero: %(default)05d')
7673+
parser.add_argument('--str', default='test',
7674+
help='str: %(default)s')
7675+
parser.add_argument('--pct', type=int, default=50,
7676+
help='pct: %(default)d%%')
7677+
parser.add_argument('--literal', help='literal: 100%%')
7678+
parser.add_argument('--prog', help='prog: %(prog)s')
7679+
parser.add_argument('--type', type=int, help='type: %(type)s')
7680+
parser.add_argument('--choices', choices=['a', 'b'],
7681+
help='choices: %(choices)s')
7682+
7683+
help_text = parser.format_help()
7684+
7685+
interp = self.theme.interpolated_value
7686+
reset = self.theme.reset
7687+
7688+
self.assertIn(f'hex: {interp}ff{reset}', help_text)
7689+
self.assertIn(f'alt: {interp}0xff{reset}', help_text)
7690+
self.assertIn(f'zero: {interp}00007{reset}', help_text)
7691+
self.assertIn(f'str: {interp}test{reset}', help_text)
7692+
self.assertIn(f'pct: {interp}50{reset}%', help_text)
7693+
self.assertIn('literal: 100%', help_text)
7694+
self.assertIn(f'prog: {interp}PROG{reset}', help_text)
7695+
self.assertIn(f'type: {interp}int{reset}', help_text)
7696+
self.assertIn(f'choices: {interp}a, b{reset}', help_text)
7697+
76667698
def test_print_help_uses_target_file_for_color_decision(self):
76677699
parser = argparse.ArgumentParser(prog='PROG', color=True)
76687700
parser.add_argument('--opt')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix regression in :mod:`argparse` where format specifiers in help strings raised :exc:`ValueError`.

0 commit comments

Comments
 (0)