Skip to content

Commit a8509df

Browse files
savannahostrowskiblurb-it[bot]hugovk
authored andcommitted
GH-142035: Fix wrapping of colorized argparse help text (GH-154634)
(cherry picked from commit 998fc4a) Co-authored-by: Savannah Ostrowski <savannah@python.org> Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent efbe54a commit a8509df

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

Lib/argparse.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,35 @@ def _split_lines(self, text, width):
776776
# The textwrap module is used only for formatting help.
777777
# Delay its import for speeding up the common usage of argparse.
778778
import textwrap
779-
return textwrap.wrap(text, width)
779+
decolored = self._decolor(text)
780+
if decolored == text:
781+
return textwrap.wrap(text, width)
782+
783+
# gh-142035: colors inflate textwrap's length counts, so wrap
784+
# the decolored text and re-apply colors per word; if textwrap
785+
# split a word, keep the plain lines (colors can't be mapped).
786+
plain = self._whitespace_matcher.sub(' ', decolored).strip()
787+
if not plain:
788+
# nothing visible to wrap (e.g. an empty interpolated value)
789+
return [text]
790+
plain_lines = textwrap.wrap(plain, width)
791+
plain_words = plain.split()
792+
colored_words = text.split()
793+
# Drop escape-only tokens (e.g. an empty interpolated value).
794+
if len(colored_words) != len(plain_words):
795+
colored_words = [
796+
word for word in colored_words if self._decolor(word)
797+
]
798+
colored_lines = []
799+
start = 0
800+
for plain_line in plain_lines:
801+
plain_line_words = plain_line.split()
802+
end = start + len(plain_line_words)
803+
if plain_words[start:end] != plain_line_words:
804+
return plain_lines
805+
colored_lines.append(' '.join(colored_words[start:end]))
806+
start = end
807+
return colored_lines
780808

781809
def _fill_text(self, text, width, indent):
782810
text = self._whitespace_matcher.sub(' ', text).strip()

Lib/test/test_argparse.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7580,6 +7580,62 @@ def test_argparse_color_custom_usage(self):
75807580
),
75817581
)
75827582

7583+
def test_argparse_color_wrapping_matches_uncolored(self):
7584+
# gh-142035: color codes must not affect where help text wraps.
7585+
# Stripping the escapes from colored help must yield exactly the
7586+
# same text as the uncolored help across representative widths.
7587+
def build(color, path="output.txt"):
7588+
parser = argparse.ArgumentParser(prog="PROG", color=color)
7589+
parser.add_argument(
7590+
"--mode",
7591+
default="auto",
7592+
choices=("auto", "fast", "slow"),
7593+
help="select the operating mode from the available choices "
7594+
"%(choices)s and note the default is %(default)s here",
7595+
)
7596+
parser.add_argument(
7597+
"--path",
7598+
default=path,
7599+
help="write output to %(default)s and continue processing",
7600+
)
7601+
return parser
7602+
7603+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7604+
paths = (
7605+
"output.txt",
7606+
"/var/lib/application/cache/unusually_long_generated_filename",
7607+
"production-read-only-replica",
7608+
)
7609+
for path in paths:
7610+
for columns in ("80", "60", "45", "30", "20"):
7611+
with self.subTest(path=path, columns=columns):
7612+
env["COLUMNS"] = columns
7613+
colored = build(color=True, path=path).format_help()
7614+
plain = build(color=False, path=path).format_help()
7615+
self.assertIn(
7616+
f"{self.theme.interpolated_value}auto"
7617+
f"{self.theme.reset}",
7618+
colored,
7619+
)
7620+
self.assertEqual(_colorize.decolor(colored), plain)
7621+
7622+
def test_argparse_color_preserved_when_wrapping_between_words(self):
7623+
parser = argparse.ArgumentParser(prog="PROG", color=True)
7624+
parser.add_argument(
7625+
"--mode", default="auto",
7626+
help="select the %(default)s operating mode from the available "
7627+
"options and continue with several more words",
7628+
)
7629+
7630+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7631+
env["COLUMNS"] = "40"
7632+
help_text = parser.format_help()
7633+
7634+
self.assertIn(
7635+
f"{self.theme.interpolated_value}auto{self.theme.reset}",
7636+
help_text,
7637+
)
7638+
75837639
def test_custom_formatter_function(self):
75847640
def custom_formatter(prog):
75857641
return argparse.RawTextHelpFormatter(prog, indent_increment=5)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect wrapping of :mod:`argparse` help text when color is enabled.

0 commit comments

Comments
 (0)