Skip to content

Commit 5d23a43

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 aba6bb9 commit 5d23a43

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
@@ -690,7 +690,35 @@ def _split_lines(self, text, width):
690690
# The textwrap module is used only for formatting help.
691691
# Delay its import for speeding up the common usage of argparse.
692692
import textwrap
693-
return textwrap.wrap(text, width)
693+
decolored = self._decolor(text)
694+
if decolored == text:
695+
return textwrap.wrap(text, width)
696+
697+
# gh-142035: colors inflate textwrap's length counts, so wrap
698+
# the decolored text and re-apply colors per word; if textwrap
699+
# split a word, keep the plain lines (colors can't be mapped).
700+
plain = self._whitespace_matcher.sub(' ', decolored).strip()
701+
if not plain:
702+
# nothing visible to wrap (e.g. an empty interpolated value)
703+
return [text]
704+
plain_lines = textwrap.wrap(plain, width)
705+
plain_words = plain.split()
706+
colored_words = text.split()
707+
# Drop escape-only tokens (e.g. an empty interpolated value).
708+
if len(colored_words) != len(plain_words):
709+
colored_words = [
710+
word for word in colored_words if self._decolor(word)
711+
]
712+
colored_lines = []
713+
start = 0
714+
for plain_line in plain_lines:
715+
plain_line_words = plain_line.split()
716+
end = start + len(plain_line_words)
717+
if plain_words[start:end] != plain_line_words:
718+
return plain_lines
719+
colored_lines.append(' '.join(colored_words[start:end]))
720+
start = end
721+
return colored_lines
694722

695723
def _fill_text(self, text, width, indent):
696724
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
@@ -7357,6 +7357,62 @@ def test_argparse_color_custom_usage(self):
73577357
),
73587358
)
73597359

7360+
def test_argparse_color_wrapping_matches_uncolored(self):
7361+
# gh-142035: color codes must not affect where help text wraps.
7362+
# Stripping the escapes from colored help must yield exactly the
7363+
# same text as the uncolored help across representative widths.
7364+
def build(color, path="output.txt"):
7365+
parser = argparse.ArgumentParser(prog="PROG", color=color)
7366+
parser.add_argument(
7367+
"--mode",
7368+
default="auto",
7369+
choices=("auto", "fast", "slow"),
7370+
help="select the operating mode from the available choices "
7371+
"%(choices)s and note the default is %(default)s here",
7372+
)
7373+
parser.add_argument(
7374+
"--path",
7375+
default=path,
7376+
help="write output to %(default)s and continue processing",
7377+
)
7378+
return parser
7379+
7380+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7381+
paths = (
7382+
"output.txt",
7383+
"/var/lib/application/cache/unusually_long_generated_filename",
7384+
"production-read-only-replica",
7385+
)
7386+
for path in paths:
7387+
for columns in ("80", "60", "45", "30", "20"):
7388+
with self.subTest(path=path, columns=columns):
7389+
env["COLUMNS"] = columns
7390+
colored = build(color=True, path=path).format_help()
7391+
plain = build(color=False, path=path).format_help()
7392+
self.assertIn(
7393+
f"{self.theme.interpolated_value}auto"
7394+
f"{self.theme.reset}",
7395+
colored,
7396+
)
7397+
self.assertEqual(_colorize.decolor(colored), plain)
7398+
7399+
def test_argparse_color_preserved_when_wrapping_between_words(self):
7400+
parser = argparse.ArgumentParser(prog="PROG", color=True)
7401+
parser.add_argument(
7402+
"--mode", default="auto",
7403+
help="select the %(default)s operating mode from the available "
7404+
"options and continue with several more words",
7405+
)
7406+
7407+
env = self.enterContext(os_helper.EnvironmentVarGuard())
7408+
env["COLUMNS"] = "40"
7409+
help_text = parser.format_help()
7410+
7411+
self.assertIn(
7412+
f"{self.theme.interpolated_value}auto{self.theme.reset}",
7413+
help_text,
7414+
)
7415+
73607416
def test_custom_formatter_function(self):
73617417
def custom_formatter(prog):
73627418
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)