Skip to content

Commit 7099af8

Browse files
frostmingblurb-it[bot]savannahostrowski
authored
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 <[email protected]>
1 parent 3fa1425 commit 7099af8

File tree

3 files changed

+65
-14
lines changed

3 files changed

+65
-14
lines changed

Lib/argparse.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@
8989
import os as _os
9090
import re as _re
9191
import sys as _sys
92-
93-
from gettext import gettext as _, ngettext
92+
from gettext import gettext as _
93+
from gettext import ngettext
9494

9595
SUPPRESS = '==SUPPRESS=='
9696

@@ -191,10 +191,10 @@ def __init__(
191191

192192
self._set_color(False)
193193

194-
def _set_color(self, color):
194+
def _set_color(self, color, *, file=None):
195195
from _colorize import can_colorize, decolor, get_theme
196196

197-
if color and can_colorize():
197+
if color and can_colorize(file=file):
198198
self._theme = get_theme(force_color=True).argparse
199199
self._decolor = decolor
200200
else:
@@ -1675,7 +1675,7 @@ def _get_optional_kwargs(self, *args, **kwargs):
16751675
option_strings = []
16761676
for option_string in args:
16771677
# error on strings that don't start with an appropriate prefix
1678-
if not option_string[0] in self.prefix_chars:
1678+
if option_string[0] not in self.prefix_chars:
16791679
raise ValueError(
16801680
f'invalid option string {option_string!r}: '
16811681
f'must start with a character {self.prefix_chars!r}')
@@ -2455,7 +2455,7 @@ def _parse_optional(self, arg_string):
24552455
return None
24562456

24572457
# if it doesn't start with a prefix, it was meant to be positional
2458-
if not arg_string[0] in self.prefix_chars:
2458+
if arg_string[0] not in self.prefix_chars:
24592459
return None
24602460

24612461
# if the option string is present in the parser, return the action
@@ -2717,14 +2717,16 @@ def _check_value(self, action, value):
27172717
# Help-formatting methods
27182718
# =======================
27192719

2720-
def format_usage(self):
2721-
formatter = self._get_formatter()
2720+
def format_usage(self, formatter=None):
2721+
if formatter is None:
2722+
formatter = self._get_formatter()
27222723
formatter.add_usage(self.usage, self._actions,
27232724
self._mutually_exclusive_groups)
27242725
return formatter.format_help()
27252726

2726-
def format_help(self):
2727-
formatter = self._get_formatter()
2727+
def format_help(self, formatter=None):
2728+
if formatter is None:
2729+
formatter = self._get_formatter()
27282730

27292731
# usage
27302732
formatter.add_usage(self.usage, self._actions,
@@ -2746,9 +2748,9 @@ def format_help(self):
27462748
# determine help from format above
27472749
return formatter.format_help()
27482750

2749-
def _get_formatter(self):
2751+
def _get_formatter(self, file=None):
27502752
formatter = self.formatter_class(prog=self.prog)
2751-
formatter._set_color(self.color)
2753+
formatter._set_color(self.color, file=file)
27522754
return formatter
27532755

27542756
def _get_validation_formatter(self):
@@ -2765,12 +2767,26 @@ def _get_validation_formatter(self):
27652767
def print_usage(self, file=None):
27662768
if file is None:
27672769
file = _sys.stdout
2768-
self._print_message(self.format_usage(), file)
2770+
formatter = self._get_formatter(file=file)
2771+
try:
2772+
usage_text = self.format_usage(formatter=formatter)
2773+
except TypeError:
2774+
# Backward compatibility for formatter classes that
2775+
# do not accept the 'formatter' keyword argument.
2776+
usage_text = self.format_usage()
2777+
self._print_message(usage_text, file)
27692778

27702779
def print_help(self, file=None):
27712780
if file is None:
27722781
file = _sys.stdout
2773-
self._print_message(self.format_help(), file)
2782+
formatter = self._get_formatter(file=file)
2783+
try:
2784+
help_text = self.format_help(formatter=formatter)
2785+
except TypeError:
2786+
# Backward compatibility for formatter classes that
2787+
# do not accept the 'formatter' keyword argument.
2788+
help_text = self.format_help()
2789+
self._print_message(help_text, file)
27742790

27752791
def _print_message(self, message, file=None):
27762792
if message:

Lib/test/test_argparse.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7558,6 +7558,40 @@ def test_error_and_warning_not_colorized_when_disabled(self):
75587558
self.assertNotIn('\x1b[', warn)
75597559
self.assertIn('warning:', warn)
75607560

7561+
def test_print_help_uses_target_file_for_color_decision(self):
7562+
parser = argparse.ArgumentParser(prog='PROG', color=True)
7563+
parser.add_argument('--opt')
7564+
output = io.StringIO()
7565+
calls = []
7566+
7567+
def fake_can_colorize(*, file=None):
7568+
calls.append(file)
7569+
return file is None
7570+
7571+
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
7572+
parser.print_help(file=output)
7573+
7574+
self.assertIs(calls[-1], output)
7575+
self.assertIn(output, calls)
7576+
self.assertNotIn('\x1b[', output.getvalue())
7577+
7578+
def test_print_usage_uses_target_file_for_color_decision(self):
7579+
parser = argparse.ArgumentParser(prog='PROG', color=True)
7580+
parser.add_argument('--opt')
7581+
output = io.StringIO()
7582+
calls = []
7583+
7584+
def fake_can_colorize(*, file=None):
7585+
calls.append(file)
7586+
return file is None
7587+
7588+
with swap_attr(_colorize, 'can_colorize', fake_can_colorize):
7589+
parser.print_usage(file=output)
7590+
7591+
self.assertIs(calls[-1], output)
7592+
self.assertIn(output, calls)
7593+
self.assertNotIn('\x1b[', output.getvalue())
7594+
75617595

75627596
class TestModule(unittest.TestCase):
75637597
def test_deprecated__version__(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Distinguish stdout and stderr when colorizing output in argparse module.

0 commit comments

Comments
 (0)