diff --git a/Lib/argparse.py b/Lib/argparse.py index 633fec69ea4615..a2a48fee019693 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -688,11 +688,29 @@ def _expand_help(self, action): params[name] = value.__name__ if params.get('choices') is not None: params['choices'] = ', '.join(map(str, params['choices'])) - # Before interpolating, wrap the values with color codes + t = self._theme - for name, value in params.items(): - params[name] = f"{t.interpolated_value}{value}{t.reset}" - return help_string % params + + if not t.reset: + return help_string % params + + # Format first to preserve types for specifiers, like %x that require int. + def colorize(match): + spec, name = match.group(0, 1) + if spec == '%%': + return '%' + if name in params: + formatted = spec % {name: params[name]} + return f'{t.interpolated_value}{formatted}{t.reset}' + return spec + + # Match %% or %(name)... format specifiers + result = _re.sub(r'%%|%\((\w+)\)[^a-z]*[a-z]', colorize, + help_string, flags=_re.IGNORECASE) + + if '%' in result: + raise ValueError(f"invalid format specifier in: {help_string!r}") + return result def _iter_indented_subactions(self, action): try: diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 758af98d5cb046..a48b80a42f8548 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7663,6 +7663,22 @@ def test_backtick_markup_special_regex_chars(self): help_text = parser.format_help() self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text) + def test_help_with_format_specifiers(self): + # GH-142950: format specifiers like %x should work with color=True + parser = argparse.ArgumentParser(prog='PROG', color=True) + parser.add_argument('--hex', type=int, default=255, + help='hex: %(default)x') + parser.add_argument('--str', default='test', + help='str: %(default)s') + + help_text = parser.format_help() + + interp = self.theme.interpolated_value + reset = self.theme.reset + + self.assertIn(f'hex: {interp}ff{reset}', help_text) + self.assertIn(f'str: {interp}test{reset}', help_text) + def test_print_help_uses_target_file_for_color_decision(self): parser = argparse.ArgumentParser(prog='PROG', color=True) parser.add_argument('--opt') diff --git a/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst b/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst new file mode 100644 index 00000000000000..219930c638384f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst @@ -0,0 +1 @@ +Fix regression in :mod:`argparse` where format specifiers in help strings raised :exc:`ValueError`.