]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-139946: Colorize error and warning messages in argparse (#140695)
authorSavannah Ostrowski <savannah@python.org>
Tue, 4 Nov 2025 16:31:35 +0000 (08:31 -0800)
committerGitHub <noreply@github.com>
Tue, 4 Nov 2025 16:31:35 +0000 (16:31 +0000)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Lib/_colorize.py
Lib/argparse.py
Lib/test/test_argparse.py
Lib/test/test_clinic.py
Lib/test/test_gzip.py
Lib/test/test_uuid.py
Lib/test/test_webbrowser.py
Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst [new file with mode: 0644]

index 63e951d6488547df7bb9683a66ea7114cb1b1e40..57b712bc068d4e18e23a08039b15c8608acd0b01 100644 (file)
@@ -170,6 +170,9 @@ class Argparse(ThemeSection):
     label: str = ANSIColors.BOLD_YELLOW
     action: str = ANSIColors.BOLD_GREEN
     reset: str = ANSIColors.RESET
+    error: str = ANSIColors.BOLD_MAGENTA
+    warning: str = ANSIColors.BOLD_YELLOW
+    message: str = ANSIColors.MAGENTA
 
 
 @dataclass(frozen=True, kw_only=True)
index 1f4413a9897eebaeb75032330be2d4ded95226d0..6b79747572f48f8a574922c771ab00ab705e34da 100644 (file)
@@ -2749,6 +2749,14 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
             except (AttributeError, OSError):
                 pass
 
+    def _get_theme(self, file=None):
+        from _colorize import can_colorize, get_theme
+
+        if self.color and can_colorize(file=file):
+            return get_theme(force_color=True).argparse
+        else:
+            return get_theme(force_no_color=True).argparse
+
     # ===============
     # Exiting methods
     # ===============
@@ -2768,13 +2776,21 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
         should either exit or raise an exception.
         """
         self.print_usage(_sys.stderr)
+        theme = self._get_theme(file=_sys.stderr)
+        fmt = _('%(prog)s: error: %(message)s\n')
+        fmt = fmt.replace('error: %(message)s',
+                        f'{theme.error}error:{theme.reset} {theme.message}%(message)s{theme.reset}')
+
         args = {'prog': self.prog, 'message': message}
-        self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
+        self.exit(2, fmt % args)
 
     def _warning(self, message):
+        theme = self._get_theme(file=_sys.stderr)
+        fmt = _('%(prog)s: warning: %(message)s\n')
+        fmt = fmt.replace('warning: %(message)s',
+                        f'{theme.warning}warning:{theme.reset} {theme.message}%(message)s{theme.reset}')
         args = {'prog': self.prog, 'message': message}
-        self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)
-
+        self._print_message(fmt % args, _sys.stderr)
 
 def __getattr__(name):
     if name == "__version__":
index d6c9c1ef2c81e88550c9b871b2716739ee53f81f..3a8be68a5468b06b6bd45253134a2e457c585cfb 100644 (file)
@@ -2283,6 +2283,7 @@ class TestNegativeNumber(ParserTestCase):
         ('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
     ]
 
+@force_not_colorized_test_class
 class TestArgumentAndSubparserSuggestions(TestCase):
     """Test error handling and suggestion when a user makes a typo"""
 
@@ -6147,6 +6148,7 @@ class TestTypeFunctionCallOnlyOnce(TestCase):
 # Check that deprecated arguments output warning
 # ==============================================
 
+@force_not_colorized_test_class
 class TestDeprecatedArguments(TestCase):
 
     def test_deprecated_option(self):
@@ -7370,6 +7372,45 @@ class TestColorized(TestCase):
         help_text = demo_parser.format_help()
         self.assertNotIn('\x1b[', help_text)
 
+    def test_error_and_warning_keywords_colorized(self):
+        parser = argparse.ArgumentParser(prog='PROG')
+        parser.add_argument('foo')
+
+        with self.assertRaises(SystemExit):
+            with captured_stderr() as stderr:
+                parser.parse_args([])
+
+        err = stderr.getvalue()
+        error_color = self.theme.error
+        reset = self.theme.reset
+        self.assertIn(f'{error_color}error:{reset}', err)
+
+        with captured_stderr() as stderr:
+            parser._warning('test warning')
+
+        warn = stderr.getvalue()
+        warning_color = self.theme.warning
+        self.assertIn(f'{warning_color}warning:{reset}', warn)
+
+    def test_error_and_warning_not_colorized_when_disabled(self):
+        parser = argparse.ArgumentParser(prog='PROG', color=False)
+        parser.add_argument('foo')
+
+        with self.assertRaises(SystemExit):
+            with captured_stderr() as stderr:
+                parser.parse_args([])
+
+        err = stderr.getvalue()
+        self.assertNotIn('\x1b[', err)
+        self.assertIn('error:', err)
+
+        with captured_stderr() as stderr:
+            parser._warning('test warning')
+
+        warn = stderr.getvalue()
+        self.assertNotIn('\x1b[', warn)
+        self.assertIn('warning:', warn)
+
 
 class TestModule(unittest.TestCase):
     def test_deprecated__version__(self):
index e0dbb062eb0372afab3a16b65a9c487f8a4454d6..e71f9fc181bb43eba366180647b6288d569fdb05 100644 (file)
@@ -4,6 +4,7 @@
 
 from functools import partial
 from test import support, test_tools
+from test.support import force_not_colorized_test_class
 from test.support import os_helper
 from test.support.os_helper import TESTFN, unlink, rmtree
 from textwrap import dedent
@@ -2758,6 +2759,7 @@ class ClinicParserTest(TestCase):
                 with self.assertRaisesRegex((AssertionError, TypeError), errmsg):
                     self.parse_function(block)
 
+@force_not_colorized_test_class
 class ClinicExternalTest(TestCase):
     maxDiff = None
 
index f14a882d3868666c2e0bab3a4b4174a0fe86560f..442d30fc970fa94d164f8956dd8056ce44e8386c 100644 (file)
@@ -11,7 +11,7 @@ import sys
 import unittest
 from subprocess import PIPE, Popen
 from test.support import catch_unraisable_exception
-from test.support import import_helper
+from test.support import force_not_colorized_test_class, import_helper
 from test.support import os_helper
 from test.support import _4G, bigmemtest, requires_subprocess
 from test.support.script_helper import assert_python_ok, assert_python_failure
@@ -1057,6 +1057,7 @@ def create_and_remove_directory(directory):
     return decorator
 
 
+@force_not_colorized_test_class
 class TestCommandLine(unittest.TestCase):
     data = b'This is a simple test with gzip'
 
index 33045a78721aac955338b4db23fdd539bbc1d7da..5f9ab048cdeb6ca706c5d68803505377fae04688 100755 (executable)
@@ -13,7 +13,7 @@ from itertools import product
 from unittest import mock
 
 from test import support
-from test.support import import_helper, warnings_helper
+from test.support import force_not_colorized_test_class, import_helper, warnings_helper
 from test.support.script_helper import assert_python_ok
 
 py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid'])
@@ -1250,10 +1250,12 @@ class CommandLineTestCases:
         self.do_test_standalone_uuid(8)
 
 
+@force_not_colorized_test_class
 class TestUUIDWithoutExtModule(CommandLineTestCases, BaseTestUUID, unittest.TestCase):
     uuid = py_uuid
 
 
+@force_not_colorized_test_class
 @unittest.skipUnless(c_uuid, 'requires the C _uuid module')
 class TestUUIDWithExtModule(CommandLineTestCases, BaseTestUUID, unittest.TestCase):
     uuid = c_uuid
index 6b577ae100e41947713eabb4785ce1aa8178dcda..20d347168b3af880263ae529700ba229a5f37ac9 100644 (file)
@@ -7,6 +7,7 @@ import sys
 import unittest
 import webbrowser
 from test import support
+from test.support import force_not_colorized_test_class
 from test.support import import_helper
 from test.support import is_apple_mobile
 from test.support import os_helper
@@ -503,6 +504,7 @@ class ImportTest(unittest.TestCase):
             self.assertEqual(webbrowser.get().name, sys.executable)
 
 
+@force_not_colorized_test_class
 class CliTest(unittest.TestCase):
     def test_parse_args(self):
         for command, url, new_win in [
diff --git a/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst b/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst
new file mode 100644 (file)
index 0000000..4c68d4c
--- /dev/null
@@ -0,0 +1 @@
+Error and warning keywords in ``argparse.ArgumentParser`` messages are now colorized when color output is enabled, fixing a visual inconsistency in which they remained plain text while other output was colorized.