From 1e3e34321d05aa566a259d7cb07d63d49136f1f2 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 19:49:15 -0700 Subject: [PATCH 1/8] Add fix to decolorize and test --- Lib/argparse.py | 2 +- Lib/test/test_argparse.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index d71e551401cb02..434aedc87d747a 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1963,7 +1963,7 @@ def add_subparsers(self, **kwargs): positionals = self._get_positional_actions() groups = self._mutually_exclusive_groups formatter.add_usage(None, positionals, groups, '') - kwargs['prog'] = formatter.format_help().strip() + kwargs['prog'] = formatter._decolor(formatter.format_help().strip()) # create the parsers action and add it to the positionals list parsers_class = self._pop_action_class(kwargs, 'parsers') diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 27e38040a98d10..c4d855862aba82 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -30,6 +30,7 @@ from unittest import mock + py = os.path.basename(sys.executable) @@ -7356,6 +7357,28 @@ def __init__(self, prog): ''')) +class TestColorEnvironment(TestCase): + """Tests for color behavior with environment variable changes.""" + + def test_subparser_respects_no_color_environment(self): + # Cleanup to ensure environment is properly reset + self.addCleanup(os.environ.pop, 'FORCE_COLOR', None) + self.addCleanup(os.environ.pop, 'NO_COLOR', None) + + # Create parser with FORCE_COLOR, capturing colored prog + os.environ['FORCE_COLOR'] = '1' + parser = argparse.ArgumentParser(prog='complex') + sub = parser.add_subparsers(dest='command') + demo_parser = sub.add_parser('demo') + + # Switch to NO_COLOR environment + os.environ.pop('FORCE_COLOR', None) + os.environ['NO_COLOR'] = '1' + + # Subparser help should have no color codes + help_text = demo_parser.format_help() + self.assertNotIn('\x1b[', help_text) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): with self.assertWarnsRegex( From 8511193b67093af083d637b9324f74ec3ea09178 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 19:50:10 -0700 Subject: [PATCH 2/8] Remove extra line --- Lib/test/test_argparse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index c4d855862aba82..fef5c31fcffb6c 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -30,7 +30,6 @@ from unittest import mock - py = os.path.basename(sys.executable) From c94021bfca88a8f2d760f1cba43504e80aa9be8f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 03:06:25 +0000 Subject: [PATCH 3/8] =?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 --- .../2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst new file mode 100644 index 00000000000000..2e2897e71b3340 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst @@ -0,0 +1 @@ +Decolor subparser ``prog`` in :meth:`argparse.ArgumentParser.add_subparsers()` to respect color environment variable changes after parser creation. From 98b78b6768bcd1aa69186dffcc0a7702ab2ae85b Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 20:08:45 -0700 Subject: [PATCH 4/8] Remove parens in blurb --- .../2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst index 2e2897e71b3340..3296694b95d49f 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst @@ -1 +1 @@ -Decolor subparser ``prog`` in :meth:`argparse.ArgumentParser.add_subparsers()` to respect color environment variable changes after parser creation. +Decolor subparser ``prog`` in :meth:`argparse.ArgumentParser.add_subparsers` to respect color environment variable changes after parser creation. From 666dd4391b283e09c600eb58dcc4eeb0a42dc007 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 21:06:53 -0700 Subject: [PATCH 5/8] Use formatter class to avoid premature colorization of prog --- Lib/argparse.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 434aedc87d747a..1ddb7abbb35355 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1959,11 +1959,13 @@ def add_subparsers(self, **kwargs): # prog defaults to the usage message of this parser, skipping # optional arguments and with no "usage:" prefix if kwargs.get('prog') is None: - formatter = self._get_formatter() + # Create formatter without color to avoid storing ANSI codes in prog + formatter = self.formatter_class(prog=self.prog) + formatter._set_color(False) positionals = self._get_positional_actions() groups = self._mutually_exclusive_groups formatter.add_usage(None, positionals, groups, '') - kwargs['prog'] = formatter._decolor(formatter.format_help().strip()) + kwargs['prog'] = formatter.format_help().strip() # create the parsers action and add it to the positionals list parsers_class = self._pop_action_class(kwargs, 'parsers') From f2c813abd4424975cfa9a60cf963286d44843a53 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 21:10:23 -0700 Subject: [PATCH 6/8] Update blurb --- .../2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst index 3296694b95d49f..498b8f7fd27bd3 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst @@ -1 +1 @@ -Decolor subparser ``prog`` in :meth:`argparse.ArgumentParser.add_subparsers` to respect color environment variable changes after parser creation. +Prevent premature colorization of subparser ``prog`` in :meth:`argparse.ArgumentParser.add_subparsers` to respect color environment variable changes after parser creation. From 5ba525a862f8ee8cf7fe4de8d7cfdc224fda3d7c Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Wed, 8 Oct 2025 21:42:52 -0700 Subject: [PATCH 7/8] Update test --- Lib/test/test_argparse.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index fef5c31fcffb6c..af5695f42241e1 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7128,7 +7128,9 @@ class TestColorized(TestCase): def setUp(self): super().setUp() # Ensure color even if ran with NO_COLOR=1 + original_can_colorize = _colorize.can_colorize _colorize.can_colorize = lambda *args, **kwargs: True + self.addCleanup(setattr, _colorize, 'can_colorize', original_can_colorize) self.theme = _colorize.get_theme(force_color=True).argparse def test_argparse_color(self): @@ -7355,29 +7357,18 @@ def __init__(self, prog): {short_b}+f{reset}, {long_b}++foo{reset} {label_b}FOO{reset} foo help ''')) - -class TestColorEnvironment(TestCase): - """Tests for color behavior with environment variable changes.""" - - def test_subparser_respects_no_color_environment(self): - # Cleanup to ensure environment is properly reset - self.addCleanup(os.environ.pop, 'FORCE_COLOR', None) - self.addCleanup(os.environ.pop, 'NO_COLOR', None) - - # Create parser with FORCE_COLOR, capturing colored prog - os.environ['FORCE_COLOR'] = '1' - parser = argparse.ArgumentParser(prog='complex') + def test_subparser_prog_is_stored_without_color(self): + parser = argparse.ArgumentParser(prog='complex', color=True) sub = parser.add_subparsers(dest='command') demo_parser = sub.add_parser('demo') - # Switch to NO_COLOR environment - os.environ.pop('FORCE_COLOR', None) - os.environ['NO_COLOR'] = '1' + self.assertNotIn('\x1b[', demo_parser.prog) - # Subparser help should have no color codes + demo_parser.color = False help_text = demo_parser.format_help() self.assertNotIn('\x1b[', help_text) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): with self.assertWarnsRegex( From 86551d179db367fe5f3d9b585946c70c15a5389e Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Thu, 9 Oct 2025 08:54:22 -0700 Subject: [PATCH 8/8] Move blurb and simplify setUp --- Lib/test/test_argparse.py | 6 +++--- .../2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename Misc/NEWS.d/next/{Core_and_Builtins => Library}/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst (100%) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index af5695f42241e1..6a07f190285c37 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -22,6 +22,7 @@ captured_stderr, force_not_colorized, force_not_colorized_test_class, + swap_attr, ) from test.support import import_helper from test.support import os_helper @@ -7128,9 +7129,8 @@ class TestColorized(TestCase): def setUp(self): super().setUp() # Ensure color even if ran with NO_COLOR=1 - original_can_colorize = _colorize.can_colorize - _colorize.can_colorize = lambda *args, **kwargs: True - self.addCleanup(setattr, _colorize, 'can_colorize', original_can_colorize) + self.enterContext(swap_attr(_colorize, 'can_colorize', + lambda *args, **kwargs: True)) self.theme = _colorize.get_theme(force_color=True).argparse def test_argparse_color(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst b/Misc/NEWS.d/next/Library/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst similarity index 100% rename from Misc/NEWS.d/next/Core_and_Builtins/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst rename to Misc/NEWS.d/next/Library/2025-10-09-03-06-19.gh-issue-139809.lzHJNu.rst