Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_MAGENTA
message: str = ANSIColors.MAGENTA


@dataclass(frozen=True, kw_only=True)
Expand Down
22 changes: 19 additions & 3 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ===============
Expand All @@ -2768,13 +2776,21 @@ 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: %(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__":
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -7370,6 +7372,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):
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'

Expand Down
4 changes: 3 additions & 1 deletion Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 [
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading