From 65174341def5e83d824b7cbbeaddb1bc5c8744d1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 28 Nov 2025 15:39:28 +0100 Subject: [PATCH 1/2] Add textwrap.wrap new ignore_ansi_escape parameter, to ignore ANSI escape codes --- Lib/argparse.py | 5 +++-- Lib/textwrap.py | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 55ecdadd8c9398..3980ab2524546e 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -691,14 +691,15 @@ def _split_lines(self, text, width): # The textwrap module is used only for formatting help. # Delay its import for speeding up the common usage of argparse. import textwrap - return textwrap.wrap(text, width) + return textwrap.wrap(text, width, ignore_ansi_escape=True) def _fill_text(self, text, width, indent): text = self._whitespace_matcher.sub(' ', text).strip() import textwrap return textwrap.fill(text, width, initial_indent=indent, - subsequent_indent=indent) + subsequent_indent=indent, + ignore_ansi_escape=True) def _get_help_string(self, action): return action.help diff --git a/Lib/textwrap.py b/Lib/textwrap.py index 41366fbf443a4f..db78cde126d3e0 100644 --- a/Lib/textwrap.py +++ b/Lib/textwrap.py @@ -61,6 +61,8 @@ class TextWrapper: Truncate wrapped lines. placeholder (default: ' [...]') Append to the last line of truncated text. + ignore_ansi_escape (default: false) + Ignore ANSI escape sequences when computing lengths of lines. """ unicode_whitespace_trans = dict.fromkeys(map(ord, _whitespace), ord(' ')) @@ -109,6 +111,8 @@ class TextWrapper: r'[\"\']?' # optional end-of-quote r'\z') # end of chunk + ansi_escape_re = re.compile(r'\x1b\[[0-9;]*m') + def __init__(self, width=70, initial_indent="", @@ -122,7 +126,8 @@ def __init__(self, tabsize=8, *, max_lines=None, - placeholder=' [...]'): + placeholder=' [...]', + ignore_ansi_escape=False): self.width = width self.initial_indent = initial_indent self.subsequent_indent = subsequent_indent @@ -135,6 +140,7 @@ def __init__(self, self.tabsize = tabsize self.max_lines = max_lines self.placeholder = placeholder + self.ignore_ansi_escape = ignore_ansi_escape # -- Private methods ----------------------------------------------- @@ -235,6 +241,10 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): # cur_len will be zero, so the next line will be entirely # devoted to the long word that we can't handle right now. + def _str_len_without_ansi_escape_codes(self, s): + """Return the length of string s without ANSI escape codes.""" + return len(self.ansi_escape_re.sub(s, "")) + def _wrap_chunks(self, chunks): """_wrap_chunks(chunks : [string]) -> [string] @@ -259,6 +269,11 @@ def _wrap_chunks(self, chunks): if len(indent) + len(self.placeholder.lstrip()) > self.width: raise ValueError("placeholder too large for max width") + if self.ignore_ansi_escape: + _str_len = self._str_len_without_ansi_escape_codes + else: + _str_len = len + # Arrange in reverse order so items can be efficiently popped # from a stack of chucks. chunks.reverse() @@ -285,7 +300,7 @@ def _wrap_chunks(self, chunks): del chunks[-1] while chunks: - l = len(chunks[-1]) + l = _str_len(chunks[-1]) # Can at least squeeze this chunk onto the current line. if cur_len + l <= width: From 56be759934431cf05ddc897d5c5bb06e10a3f3b7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 29 Nov 2025 11:13:51 +0100 Subject: [PATCH 2/2] Fix order of re.sub parameters --- Lib/textwrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/textwrap.py b/Lib/textwrap.py index db78cde126d3e0..c41e430a7f8d96 100644 --- a/Lib/textwrap.py +++ b/Lib/textwrap.py @@ -243,7 +243,7 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): def _str_len_without_ansi_escape_codes(self, s): """Return the length of string s without ANSI escape codes.""" - return len(self.ansi_escape_re.sub(s, "")) + return len(self.ansi_escape_re.sub("", s)) def _wrap_chunks(self, chunks): """_wrap_chunks(chunks : [string]) -> [string]