mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
gh-139946: distinguish stdout or stderr when colorizing output in argparse (#140495)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Savannah Ostrowski <savannah@python.org>
This commit is contained in:
parent
3fa1425bfb
commit
7099af8f5e
3 changed files with 65 additions and 14 deletions
|
|
@ -89,8 +89,8 @@
|
||||||
import os as _os
|
import os as _os
|
||||||
import re as _re
|
import re as _re
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
|
from gettext import gettext as _
|
||||||
from gettext import gettext as _, ngettext
|
from gettext import ngettext
|
||||||
|
|
||||||
SUPPRESS = '==SUPPRESS=='
|
SUPPRESS = '==SUPPRESS=='
|
||||||
|
|
||||||
|
|
@ -191,10 +191,10 @@ def __init__(
|
||||||
|
|
||||||
self._set_color(False)
|
self._set_color(False)
|
||||||
|
|
||||||
def _set_color(self, color):
|
def _set_color(self, color, *, file=None):
|
||||||
from _colorize import can_colorize, decolor, get_theme
|
from _colorize import can_colorize, decolor, get_theme
|
||||||
|
|
||||||
if color and can_colorize():
|
if color and can_colorize(file=file):
|
||||||
self._theme = get_theme(force_color=True).argparse
|
self._theme = get_theme(force_color=True).argparse
|
||||||
self._decolor = decolor
|
self._decolor = decolor
|
||||||
else:
|
else:
|
||||||
|
|
@ -1675,7 +1675,7 @@ def _get_optional_kwargs(self, *args, **kwargs):
|
||||||
option_strings = []
|
option_strings = []
|
||||||
for option_string in args:
|
for option_string in args:
|
||||||
# error on strings that don't start with an appropriate prefix
|
# error on strings that don't start with an appropriate prefix
|
||||||
if not option_string[0] in self.prefix_chars:
|
if option_string[0] not in self.prefix_chars:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'invalid option string {option_string!r}: '
|
f'invalid option string {option_string!r}: '
|
||||||
f'must start with a character {self.prefix_chars!r}')
|
f'must start with a character {self.prefix_chars!r}')
|
||||||
|
|
@ -2455,7 +2455,7 @@ def _parse_optional(self, arg_string):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# if it doesn't start with a prefix, it was meant to be positional
|
# if it doesn't start with a prefix, it was meant to be positional
|
||||||
if not arg_string[0] in self.prefix_chars:
|
if arg_string[0] not in self.prefix_chars:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# if the option string is present in the parser, return the action
|
# if the option string is present in the parser, return the action
|
||||||
|
|
@ -2717,13 +2717,15 @@ def _check_value(self, action, value):
|
||||||
# Help-formatting methods
|
# Help-formatting methods
|
||||||
# =======================
|
# =======================
|
||||||
|
|
||||||
def format_usage(self):
|
def format_usage(self, formatter=None):
|
||||||
|
if formatter is None:
|
||||||
formatter = self._get_formatter()
|
formatter = self._get_formatter()
|
||||||
formatter.add_usage(self.usage, self._actions,
|
formatter.add_usage(self.usage, self._actions,
|
||||||
self._mutually_exclusive_groups)
|
self._mutually_exclusive_groups)
|
||||||
return formatter.format_help()
|
return formatter.format_help()
|
||||||
|
|
||||||
def format_help(self):
|
def format_help(self, formatter=None):
|
||||||
|
if formatter is None:
|
||||||
formatter = self._get_formatter()
|
formatter = self._get_formatter()
|
||||||
|
|
||||||
# usage
|
# usage
|
||||||
|
|
@ -2746,9 +2748,9 @@ def format_help(self):
|
||||||
# determine help from format above
|
# determine help from format above
|
||||||
return formatter.format_help()
|
return formatter.format_help()
|
||||||
|
|
||||||
def _get_formatter(self):
|
def _get_formatter(self, file=None):
|
||||||
formatter = self.formatter_class(prog=self.prog)
|
formatter = self.formatter_class(prog=self.prog)
|
||||||
formatter._set_color(self.color)
|
formatter._set_color(self.color, file=file)
|
||||||
return formatter
|
return formatter
|
||||||
|
|
||||||
def _get_validation_formatter(self):
|
def _get_validation_formatter(self):
|
||||||
|
|
@ -2765,12 +2767,26 @@ def _get_validation_formatter(self):
|
||||||
def print_usage(self, file=None):
|
def print_usage(self, file=None):
|
||||||
if file is None:
|
if file is None:
|
||||||
file = _sys.stdout
|
file = _sys.stdout
|
||||||
self._print_message(self.format_usage(), file)
|
formatter = self._get_formatter(file=file)
|
||||||
|
try:
|
||||||
|
usage_text = self.format_usage(formatter=formatter)
|
||||||
|
except TypeError:
|
||||||
|
# Backward compatibility for formatter classes that
|
||||||
|
# do not accept the 'formatter' keyword argument.
|
||||||
|
usage_text = self.format_usage()
|
||||||
|
self._print_message(usage_text, file)
|
||||||
|
|
||||||
def print_help(self, file=None):
|
def print_help(self, file=None):
|
||||||
if file is None:
|
if file is None:
|
||||||
file = _sys.stdout
|
file = _sys.stdout
|
||||||
self._print_message(self.format_help(), file)
|
formatter = self._get_formatter(file=file)
|
||||||
|
try:
|
||||||
|
help_text = self.format_help(formatter=formatter)
|
||||||
|
except TypeError:
|
||||||
|
# Backward compatibility for formatter classes that
|
||||||
|
# do not accept the 'formatter' keyword argument.
|
||||||
|
help_text = self.format_help()
|
||||||
|
self._print_message(help_text, file)
|
||||||
|
|
||||||
def _print_message(self, message, file=None):
|
def _print_message(self, message, file=None):
|
||||||
if message:
|
if message:
|
||||||
|
|
|
||||||
|
|
@ -7558,6 +7558,40 @@ def test_error_and_warning_not_colorized_when_disabled(self):
|
||||||
self.assertNotIn('\x1b[', warn)
|
self.assertNotIn('\x1b[', warn)
|
||||||
self.assertIn('warning:', warn)
|
self.assertIn('warning:', warn)
|
||||||
|
|
||||||
|
def test_print_help_uses_target_file_for_color_decision(self):
|
||||||
|
parser = argparse.ArgumentParser(prog='PROG', color=True)
|
||||||
|
parser.add_argument('--opt')
|
||||||
|
output = io.StringIO()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_can_colorize(*, file=None):
|
||||||
|
calls.append(file)
|
||||||
|
return file is None
|
||||||
|
|
||||||
|
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
|
||||||
|
parser.print_help(file=output)
|
||||||
|
|
||||||
|
self.assertIs(calls[-1], output)
|
||||||
|
self.assertIn(output, calls)
|
||||||
|
self.assertNotIn('\x1b[', output.getvalue())
|
||||||
|
|
||||||
|
def test_print_usage_uses_target_file_for_color_decision(self):
|
||||||
|
parser = argparse.ArgumentParser(prog='PROG', color=True)
|
||||||
|
parser.add_argument('--opt')
|
||||||
|
output = io.StringIO()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_can_colorize(*, file=None):
|
||||||
|
calls.append(file)
|
||||||
|
return file is None
|
||||||
|
|
||||||
|
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
|
||||||
|
parser.print_usage(file=output)
|
||||||
|
|
||||||
|
self.assertIs(calls[-1], output)
|
||||||
|
self.assertIn(output, calls)
|
||||||
|
self.assertNotIn('\x1b[', output.getvalue())
|
||||||
|
|
||||||
|
|
||||||
class TestModule(unittest.TestCase):
|
class TestModule(unittest.TestCase):
|
||||||
def test_deprecated__version__(self):
|
def test_deprecated__version__(self):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Distinguish stdout and stderr when colorizing output in argparse module.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue