From 0917e2c69a022ab157928aad2d048bd85b0a9078 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Mon, 27 Oct 2025 19:32:04 -0700 Subject: [PATCH 1/5] Colorize error and warning messages in argparse --- Lib/_colorize.py | 2 ++ Lib/argparse.py | 20 +++++++++++++++++--- Lib/test/test_argparse.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 63e951d6488547..af4c257cc4a2d1 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -170,6 +170,8 @@ class Argparse(ThemeSection): label: str = ANSIColors.BOLD_YELLOW action: str = ANSIColors.BOLD_GREEN reset: str = ANSIColors.RESET + error: str = ANSIColors.BOLD_RED + warning: str = ANSIColors.BOLD_YELLOW @dataclass(frozen=True, kw_only=True) diff --git a/Lib/argparse.py b/Lib/argparse.py index 1f4413a9897eeb..625575835fa4ab 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2749,6 +2749,14 @@ def _print_message(self, message, file=None): 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,19 @@ def error(self, message): 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:', f'{theme.error}error:{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:', f'{theme.warning}warning:{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__": diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d6c9c1ef2c81e8..d852ebc563795e 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7370,6 +7370,45 @@ def test_subparser_prog_is_stored_without_color(self): 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): From 1b4a6e9ccf1e04e1c8b78de6debe93de4590d75e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:47:00 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst 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 index 00000000000000..4c68d4cd94bf78 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-28-02-46-56.gh-issue-139946.aN3_uY.rst @@ -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. From 71f9d76ebdce55997ee77bafbe3ff65fb9093ae8 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Mon, 27 Oct 2025 21:55:36 -0700 Subject: [PATCH 3/5] Add force_not_colorized --- Lib/test/test_argparse.py | 2 ++ Lib/test/test_clinic.py | 2 ++ Lib/test/test_gzip.py | 3 ++- Lib/test/test_uuid.py | 4 +++- Lib/test/test_webbrowser.py | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d852ebc563795e..3a8be68a5468b0 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -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 @@ def spam(string_to_convert): # Check that deprecated arguments output warning # ============================================== +@force_not_colorized_test_class class TestDeprecatedArguments(TestCase): def test_deprecated_option(self): diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index e0dbb062eb0372..e71f9fc181bb43 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -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 @@ def test_allow_negative_accepted_by_py_ssize_t_converter_only(self): with self.assertRaisesRegex((AssertionError, TypeError), errmsg): self.parse_function(block) +@force_not_colorized_test_class class ClinicExternalTest(TestCase): maxDiff = None diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index f14a882d386866..442d30fc970fa9 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -11,7 +11,7 @@ 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 wrapper(*args, **kwargs): return decorator +@force_not_colorized_test_class class TestCommandLine(unittest.TestCase): data = b'This is a simple test with gzip' diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 33045a78721aac..5f9ab048cdeb6c 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -13,7 +13,7 @@ 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 @@ def test_cli_uuid8(self): 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 diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 6b577ae100e419..20d347168b3af8 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -7,6 +7,7 @@ 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 @@ def test_environment_preferred(self): 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 [ From 7f9e78b8024530dedd8c21856706ffcccd0464f6 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 29 Oct 2025 20:25:47 -0700 Subject: [PATCH 4/5] Address PR comments to use same color scheme and color message --- Lib/_colorize.py | 5 +++-- Lib/argparse.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index af4c257cc4a2d1..d7a4ac2eb4d491 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -170,8 +170,9 @@ class Argparse(ThemeSection): label: str = ANSIColors.BOLD_YELLOW action: str = ANSIColors.BOLD_GREEN reset: str = ANSIColors.RESET - error: str = ANSIColors.BOLD_RED - warning: str = ANSIColors.BOLD_YELLOW + error: str = ANSIColors.BOLD_MAGENTA + warning: str = ANSIColors.BOLD_MAGENTA + message: str = ANSIColors.MAGENTA @dataclass(frozen=True, kw_only=True) diff --git a/Lib/argparse.py b/Lib/argparse.py index 625575835fa4ab..6b79747572f48f 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2778,7 +2778,8 @@ def error(self, message): self.print_usage(_sys.stderr) theme = self._get_theme(file=_sys.stderr) fmt = _('%(prog)s: error: %(message)s\n') - fmt = fmt.replace('error:', f'{theme.error}error:{theme.reset}') + 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, fmt % args) @@ -2786,7 +2787,8 @@ def error(self, message): def _warning(self, message): theme = self._get_theme(file=_sys.stderr) fmt = _('%(prog)s: warning: %(message)s\n') - fmt = fmt.replace('warning:', f'{theme.warning}warning:{theme.reset}') + 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(fmt % args, _sys.stderr) From dc78998d4471a6b1f2ae862e592e5406e14c35d6 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 4 Nov 2025 07:53:38 -0800 Subject: [PATCH 5/5] Update Lib/_colorize.py Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/_colorize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index d7a4ac2eb4d491..57b712bc068d4e 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -171,7 +171,7 @@ class Argparse(ThemeSection): action: str = ANSIColors.BOLD_GREEN reset: str = ANSIColors.RESET error: str = ANSIColors.BOLD_MAGENTA - warning: str = ANSIColors.BOLD_MAGENTA + warning: str = ANSIColors.BOLD_YELLOW message: str = ANSIColors.MAGENTA