]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-142950: Process format specifiers before colourization in argparse help (#142960)
authorSavannah Ostrowski <savannah@python.org>
Wed, 7 Jan 2026 17:39:47 +0000 (09:39 -0800)
committerGitHub <noreply@github.com>
Wed, 7 Jan 2026 17:39:47 +0000 (17:39 +0000)
Lib/argparse.py
Lib/test/test_argparse.py
Misc/NEWS.d/next/Library/2025-12-18-22-58-46.gh-issue-142950.EJ8w-T.rst [new file with mode: 0644]

index 633fec69ea461572a1ff677d1b7ad08cd2a2ba13..0494b545f2f1d3d885129d7832057811cf96957d 100644 (file)
@@ -688,11 +688,41 @@ class HelpFormatter(object):
                 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
+
+        result = help_string % params
+
+        if not t.reset:
+            return result
+
+        # Match format specifiers like: %s, %d, %(key)s, etc.
+        fmt_spec = r'''
+            %
+            (?:
+                %                           # %% escape
+                |
+                (?:\((?P<key>[^)]*)\))?     # key
+                [-#0\ +]*                   # flags
+                (?:\*|\d+)?                 # width
+                (?:\.(?:\*|\d+))?           # precision
+                [hlL]?                      # length modifier
+                [diouxXeEfFgGcrsa]          # conversion type
+            )
+        '''
+
+        def colorize(match):
+            spec, key = match.group(0, 'key')
+            if spec == '%%':
+                return '%'
+            if key is not None:
+                # %(key)... - format and colorize
+                formatted = spec % {key: params[key]}
+                return f'{t.interpolated_value}{formatted}{t.reset}'
+            # bare %s etc. - format with full params dict, no colorization
+            return spec % params
+
+        return _re.sub(fmt_spec, colorize, help_string, flags=_re.VERBOSE)
 
     def _iter_indented_subactions(self, action):
         try:
index 758af98d5cb04616ad1ab4e81044328389722c91..771702446754749c532d58182bff277ee7c7f385 100644 (file)
@@ -7663,6 +7663,38 @@ class TestColorized(TestCase):
         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, alt: %(default)#x')
+        parser.add_argument('--zero', type=int, default=7,
+                            help='zero: %(default)05d')
+        parser.add_argument('--str', default='test',
+                            help='str: %(default)s')
+        parser.add_argument('--pct', type=int, default=50,
+                            help='pct: %(default)d%%')
+        parser.add_argument('--literal', help='literal: 100%%')
+        parser.add_argument('--prog', help='prog: %(prog)s')
+        parser.add_argument('--type', type=int, help='type: %(type)s')
+        parser.add_argument('--choices', choices=['a', 'b'],
+                            help='choices: %(choices)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'alt: {interp}0xff{reset}', help_text)
+        self.assertIn(f'zero: {interp}00007{reset}', help_text)
+        self.assertIn(f'str: {interp}test{reset}', help_text)
+        self.assertIn(f'pct: {interp}50{reset}%', help_text)
+        self.assertIn('literal: 100%', help_text)
+        self.assertIn(f'prog: {interp}PROG{reset}', help_text)
+        self.assertIn(f'type: {interp}int{reset}', help_text)
+        self.assertIn(f'choices: {interp}a, b{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 (file)
index 0000000..219930c
--- /dev/null
@@ -0,0 +1 @@
+Fix regression in :mod:`argparse` where format specifiers in help strings raised :exc:`ValueError`.