From a3d76a20628ebb537b6ade3eb6280ac0fdc6045e Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Thu, 23 Oct 2025 14:35:21 +0800 Subject: [PATCH 1/4] fix: distinguish stdout or stderr when colorizing output in argparse Signed-off-by: Frost Ming --- Lib/argparse.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 1f4413a9897eeb..6eb69281da029e 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -191,10 +191,10 @@ def __init__( self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII) self._long_break_matcher = _re.compile(r'\n\n\n+') - def _set_color(self, color): + def _set_color(self, color, *, file=None): 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._decolor = decolor else: @@ -2693,14 +2693,16 @@ def _check_value(self, action, value): # Help-formatting methods # ======================= - def format_usage(self): - formatter = self._get_formatter() + def format_usage(self, formatter=None): + if formatter is None: + formatter = self._get_formatter() formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) return formatter.format_help() - def format_help(self): - formatter = self._get_formatter() + def format_help(self, formatter=None): + if formatter is None: + formatter = self._get_formatter() # usage formatter.add_usage(self.usage, self._actions, @@ -2734,12 +2736,16 @@ def _get_formatter(self): def print_usage(self, file=None): if file is None: file = _sys.stdout - self._print_message(self.format_usage(), file) + formatter = self._get_formatter() + formatter._set_color(self.color, file=file) + self._print_message(self.format_usage(formatter=formatter), file) def print_help(self, file=None): if file is None: file = _sys.stdout - self._print_message(self.format_help(), file) + formatter = self._get_formatter() + formatter._set_color(self.color, file=file) + self._print_message(self.format_help(formatter=formatter), file) def _print_message(self, message, file=None): if message: From a7db07475dcc62a0f5b44124f1c87b21cadc82d9 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 06:38:44 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-10-23-06-38-35.gh-issue-139946.HZa5hu.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-23-06-38-35.gh-issue-139946.HZa5hu.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-23-06-38-35.gh-issue-139946.HZa5hu.rst b/Misc/NEWS.d/next/Library/2025-10-23-06-38-35.gh-issue-139946.HZa5hu.rst new file mode 100644 index 00000000000000..fb47931728414d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-23-06-38-35.gh-issue-139946.HZa5hu.rst @@ -0,0 +1 @@ +Distinguish stdout and stderr when colorizing output in argparse module. From 4062e6c4e8eec969a7467c021ef5ffe5749584e4 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Mon, 27 Oct 2025 08:23:17 +0800 Subject: [PATCH 3/4] fix: make _get_formatter to take file argument to avoid calling set_color Signed-off-by: Frost Ming --- Lib/argparse.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 6eb69281da029e..3f3adf3f88bce1 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2724,9 +2724,9 @@ def format_help(self, formatter=None): # determine help from format above return formatter.format_help() - def _get_formatter(self): + def _get_formatter(self, file=None): formatter = self.formatter_class(prog=self.prog) - formatter._set_color(self.color) + formatter._set_color(self.color, file=file) return formatter # ===================== @@ -2736,15 +2736,13 @@ def _get_formatter(self): def print_usage(self, file=None): if file is None: file = _sys.stdout - formatter = self._get_formatter() - formatter._set_color(self.color, file=file) + formatter = self._get_formatter(file=file) self._print_message(self.format_usage(formatter=formatter), file) def print_help(self, file=None): if file is None: file = _sys.stdout - formatter = self._get_formatter() - formatter._set_color(self.color, file=file) + formatter = self._get_formatter(file=file) self._print_message(self.format_help(formatter=formatter), file) def _print_message(self, message, file=None): From fe5c9d74718026d4cdb6128d6f930cf147251e46 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Mon, 27 Oct 2025 15:02:43 +0800 Subject: [PATCH 4/4] add tests for the change Signed-off-by: Frost Ming --- Lib/test/test_argparse.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d6c9c1ef2c81e8..f529e87bbd3995 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7370,6 +7370,40 @@ def test_subparser_prog_is_stored_without_color(self): help_text = demo_parser.format_help() self.assertNotIn('\x1b[', help_text) + 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): def test_deprecated__version__(self):