GH-139946: Colorize error and warning messages in argparse (#140695)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1326d2a808
commit
40096da95a
@@ -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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user