Skip to content

Commit bbc5ff1

Browse files
authored
Merge pull request #308 from devdanzin/master
Fix column wrapping breaking ANSI escape codes (fixes #307)
2 parents 46b35b4 + e1c59c3 commit bbc5ff1

File tree

3 files changed

+53
-6
lines changed

3 files changed

+53
-6
lines changed

tabulate/__init__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2676,10 +2676,21 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
26762676
# take each charcter's width into account
26772677
chunk = reversed_chunks[-1]
26782678
i = 1
2679-
while self._len(chunk[:i]) <= space_left:
2679+
# Only count printable characters, so strip_ansi first, index later.
2680+
while len(_strip_ansi(chunk)[:i]) <= space_left:
26802681
i = i + 1
2681-
cur_line.append(chunk[: i - 1])
2682-
reversed_chunks[-1] = chunk[i - 1 :]
2682+
# Consider escape codes when breaking words up
2683+
total_escape_len = 0
2684+
last_group = 0
2685+
if _ansi_codes.search(chunk) is not None:
2686+
for group, _, _, _ in _ansi_codes.findall(chunk):
2687+
escape_len = len(group)
2688+
if group in chunk[last_group: i + total_escape_len + escape_len - 1]:
2689+
total_escape_len += escape_len
2690+
found = _ansi_codes.search(chunk[last_group:])
2691+
last_group += found.end()
2692+
cur_line.append(chunk[: i + total_escape_len - 1])
2693+
reversed_chunks[-1] = chunk[i + total_escape_len - 1 :]
26832694

26842695
# Otherwise, we have to preserve the long word intact. Only add
26852696
# it to the current line if there's nothing already there --

test/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55

66
def assert_equal(expected, result):
7-
print("Expected:\n%s\n" % expected)
8-
print("Got:\n%s\n" % result)
7+
print("Expected:\n%r\n" % expected)
8+
print("Got:\n%r\n" % result)
99
assert expected == result
1010

1111

test/test_textwrapper.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Discretely test functionality of our custom TextWrapper"""
22
import datetime
33

4-
from tabulate import _CustomTextWrap as CTW, tabulate
4+
from tabulate import _CustomTextWrap as CTW, tabulate, _strip_ansi
55
from textwrap import TextWrapper as OTW
66

77
from common import skip, assert_equal
@@ -157,6 +157,42 @@ def test_wrap_color_line_splillover():
157157
assert_equal(expected, result)
158158

159159

160+
def test_wrap_color_line_longword():
161+
"""TextWrapper: Wrap a line - preserve internal color tags and wrap them to
162+
other lines when required, requires adding the colors tags to other lines as appropriate
163+
and avoiding splitting escape codes."""
164+
data = "This_is_a_\033[31mtest_string_for_testing_TextWrap\033[0m_with_colors"
165+
166+
expected = [
167+
"This_is_a_\033[31mte\033[0m",
168+
"\033[31mst_string_fo\033[0m",
169+
"\033[31mr_testing_Te\033[0m",
170+
"\033[31mxtWrap\033[0m_with_",
171+
"colors",
172+
]
173+
wrapper = CTW(width=12)
174+
result = wrapper.wrap(data)
175+
assert_equal(expected, result)
176+
177+
178+
def test_wrap_color_line_multiple_escapes():
179+
data = "012345(\x1b[32ma\x1b[0mbc\x1b[32mdefghij\x1b[0m)"
180+
expected = [
181+
"012345(\x1b[32ma\x1b[0mbc\x1b[32m\x1b[0m",
182+
"\x1b[32mdefghij\x1b[0m)",
183+
]
184+
wrapper = CTW(width=10)
185+
result = wrapper.wrap(data)
186+
assert_equal(expected, result)
187+
188+
clean_data = _strip_ansi(data)
189+
for width in range(2, len(clean_data)):
190+
wrapper = CTW(width=width)
191+
result = wrapper.wrap(data)
192+
# Comparing after stripping ANSI should be enough to catch broken escape codes
193+
assert_equal(clean_data, _strip_ansi("".join(result)))
194+
195+
160196
def test_wrap_datetime():
161197
"""TextWrapper: Show that datetimes can be wrapped without crashing"""
162198
data = [

0 commit comments

Comments
 (0)