Skip to content

Commit 40096da

Browse files
GH-139946: Colorize error and warning messages in argparse (#140695)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 1326d2a commit 40096da

File tree

8 files changed

+73
-5
lines changed

8 files changed

+73
-5
lines changed

Lib/_colorize.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ class Argparse(ThemeSection):
170170
label: str = ANSIColors.BOLD_YELLOW
171171
action: str = ANSIColors.BOLD_GREEN
172172
reset: str = ANSIColors.RESET
173+
error: str = ANSIColors.BOLD_MAGENTA
174+
warning: str = ANSIColors.BOLD_YELLOW
175+
message: str = ANSIColors.MAGENTA
173176

174177

175178
@dataclass(frozen=True, kw_only=True)

Lib/argparse.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,14 @@ def _print_message(self, message, file=None):
27492749
except (AttributeError, OSError):
27502750
pass
27512751

2752+
def _get_theme(self, file=None):
2753+
from _colorize import can_colorize, get_theme
2754+
2755+
if self.color and can_colorize(file=file):
2756+
return get_theme(force_color=True).argparse
2757+
else:
2758+
return get_theme(force_no_color=True).argparse
2759+
27522760
# ===============
27532761
# Exiting methods
27542762
# ===============
@@ -2768,13 +2776,21 @@ def error(self, message):
27682776
should either exit or raise an exception.
27692777
"""
27702778
self.print_usage(_sys.stderr)
2779+
theme = self._get_theme(file=_sys.stderr)
2780+
fmt = _('%(prog)s: error: %(message)s\n')
2781+
fmt = fmt.replace('error: %(message)s',
2782+
f'{theme.error}error:{theme.reset} {theme.message}%(message)s{theme.reset}')
2783+
27712784
args = {'prog': self.prog, 'message': message}
2772-
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
2785+
self.exit(2, fmt % args)
27732786

27742787
def _warning(self, message):
2788+
theme = self._get_theme(file=_sys.stderr)
2789+
fmt = _('%(prog)s: warning: %(message)s\n')
2790+
fmt = fmt.replace('warning: %(message)s',
2791+
f'{theme.warning}warning:{theme.reset} {theme.message}%(message)s{theme.reset}')
27752792
args = {'prog': self.prog, 'message': message}
2776-
self._print_message(_('%(prog)s: warning: %(message)s\n') % args, _sys.stderr)
2777-
2793+
self._print_message(fmt % args, _sys.stderr)
27782794

27792795
def __getattr__(name):
27802796
if name == "__version__":

Lib/test/test_argparse.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,6 +2283,7 @@ class TestNegativeNumber(ParserTestCase):
22832283
('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
22842284
]
22852285

2286+
@force_not_colorized_test_class
22862287
class TestArgumentAndSubparserSuggestions(TestCase):
22872288
"""Test error handling and suggestion when a user makes a typo"""
22882289

@@ -6147,6 +6148,7 @@ def spam(string_to_convert):
61476148
# Check that deprecated arguments output warning
61486149
# ==============================================
61496150

6151+
@force_not_colorized_test_class
61506152
class TestDeprecatedArguments(TestCase):
61516153

61526154
def test_deprecated_option(self):
@@ -7370,6 +7372,45 @@ def test_subparser_prog_is_stored_without_color(self):
73707372
help_text = demo_parser.format_help()
73717373
self.assertNotIn('\x1b[', help_text)
73727374

7375+
def test_error_and_warning_keywords_colorized(self):
7376+
parser = argparse.ArgumentParser(prog='PROG')
7377+
parser.add_argument('foo')
7378+
7379+
with self.assertRaises(SystemExit):
7380+
with captured_stderr() as stderr:
7381+
parser.parse_args([])
7382+
7383+
err = stderr.getvalue()
7384+
error_color = self.theme.error
7385+
reset = self.theme.reset
7386+
self.assertIn(f'{error_color}error:{reset}', err)
7387+
7388+
with captured_stderr() as stderr:
7389+
parser._warning('test warning')
7390+
7391+
warn = stderr.getvalue()
7392+
warning_color = self.theme.warning
7393+
self.assertIn(f'{warning_color}warning:{reset}', warn)
7394+
7395+
def test_error_and_warning_not_colorized_when_disabled(self):
7396+
parser = argparse.ArgumentParser(prog='PROG', color=False)
7397+
parser.add_argument('foo')
7398+
7399+
with self.assertRaises(SystemExit):
7400+
with captured_stderr() as stderr:
7401+
parser.parse_args([])
7402+
7403+
err = stderr.getvalue()
7404+
self.assertNotIn('\x1b[', err)
7405+
self.assertIn('error:', err)
7406+
7407+
with captured_stderr() as stderr:
7408+
parser._warning('test warning')
7409+
7410+
warn = stderr.getvalue()
7411+
self.assertNotIn('\x1b[', warn)
7412+
self.assertIn('warning:', warn)
7413+
73737414

73747415
class TestModule(unittest.TestCase):
73757416
def test_deprecated__version__(self):

Lib/test/test_clinic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from functools import partial
66
from test import support, test_tools
7+
from test.support import force_not_colorized_test_class
78
from test.support import os_helper
89
from test.support.os_helper import TESTFN, unlink, rmtree
910
from textwrap import dedent
@@ -2758,6 +2759,7 @@ def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):
27582759
with self.assertRaisesRegex((AssertionError, TypeError), errmsg):
27592760
self.parse_function(block)
27602761

2762+
@force_not_colorized_test_class
27612763
class ClinicExternalTest(TestCase):
27622764
maxDiff = None
27632765

Lib/test/test_gzip.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import unittest
1212
from subprocess import PIPE, Popen
1313
from test.support import catch_unraisable_exception
14-
from test.support import import_helper
14+
from test.support import force_not_colorized_test_class, import_helper
1515
from test.support import os_helper
1616
from test.support import _4G, bigmemtest, requires_subprocess
1717
from test.support.script_helper import assert_python_ok, assert_python_failure
@@ -1057,6 +1057,7 @@ def wrapper(*args, **kwargs):
10571057
return decorator
10581058

10591059

1060+
@force_not_colorized_test_class
10601061
class TestCommandLine(unittest.TestCase):
10611062
data = b'This is a simple test with gzip'
10621063

Lib/test/test_uuid.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from unittest import mock
1414

1515
from test import support
16-
from test.support import import_helper, warnings_helper
16+
from test.support import force_not_colorized_test_class, import_helper, warnings_helper
1717
from test.support.script_helper import assert_python_ok
1818

1919
py_uuid = import_helper.import_fresh_module('uuid', blocked=['_uuid'])
@@ -1250,10 +1250,12 @@ def test_cli_uuid8(self):
12501250
self.do_test_standalone_uuid(8)
12511251

12521252

1253+
@force_not_colorized_test_class
12531254
class TestUUIDWithoutExtModule(CommandLineTestCases, BaseTestUUID, unittest.TestCase):
12541255
uuid = py_uuid
12551256

12561257

1258+
@force_not_colorized_test_class
12571259
@unittest.skipUnless(c_uuid, 'requires the C _uuid module')
12581260
class TestUUIDWithExtModule(CommandLineTestCases, BaseTestUUID, unittest.TestCase):
12591261
uuid = c_uuid

Lib/test/test_webbrowser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import unittest
88
import webbrowser
99
from test import support
10+
from test.support import force_not_colorized_test_class
1011
from test.support import import_helper
1112
from test.support import is_apple_mobile
1213
from test.support import os_helper
@@ -503,6 +504,7 @@ def test_environment_preferred(self):
503504
self.assertEqual(webbrowser.get().name, sys.executable)
504505

505506

507+
@force_not_colorized_test_class
506508
class CliTest(unittest.TestCase):
507509
def test_parse_args(self):
508510
for command, url, new_win in [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)