Skip to content

Commit 4c9a049

Browse files
jquastastanin
authored andcommitted
add fallback for measuring visible width with wcwidth < 0.3
based on pull request #391
1 parent 8978756 commit 4c9a049

3 files changed

Lines changed: 57 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,4 +1280,4 @@ Dimitri Papadopoulos, Élie Goudout, Racerroar888, Phill Zarfos,
12801280
Keyacom, Andrew Coffey, Arpit Jain, Israel Roldan, ilya112358,
12811281
Dan Nicholson, Frederik Scheerer, cdar07 (cdar), Racerroar888,
12821282
Perry Kundert, Hnasar, Jun Koo, Jo2234, Bjorn Olsen, George Schizas,
1283-
Kadir Can Ozden.
1283+
Kadir Can Ozden, Jeff Quast.

tabulate/__init__.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,13 +1111,21 @@ def _visible_width(s):
11111111
"""
11121112
# optional wide-character support
11131113
if wcwidth is not None and WIDE_CHARS_MODE:
1114-
len_fn = wcwidth.wcswidth
1115-
else:
1116-
len_fn = len
1114+
# when already a string, it could contain terminal sequences,
1115+
# wcwidth >= 0.3.0 handles ANSI codes internally,
1116+
if hasattr(wcwidth, "width"):
1117+
return wcwidth.width(str(s))
1118+
# while previous versions need them stripped first.
1119+
if isinstance(s, (str, bytes)):
1120+
return wcwidth.wcswidth(_strip_ansi(str(s)))
1121+
1122+
# Otherwise, coerce to string, guaranteed to be without any control codes,
1123+
# we can use wcswidth() directly.
1124+
return wcwidth.wcswidth(str(s))
11171125
if isinstance(s, (str, bytes)):
1118-
return len_fn(_strip_ansi(s))
1126+
return len(_strip_ansi(s))
11191127
else:
1120-
return len_fn(str(s))
1128+
return len(str(s))
11211129

11221130

11231131
def _is_multiline(s):

test/test_grapheme_clusters.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for Unicode grapheme cluster handling in tabulate."""
22

3+
import unittest.mock as mock
4+
35
import pytest
46

57
from tabulate import tabulate
@@ -9,15 +11,21 @@
911

1012
HAS_WCWIDTH = True
1113
HAS_WCWIDTH_030 = hasattr(wcwidth, "wrap")
14+
HAS_WCWIDTH_WIDTH = hasattr(wcwidth, "width")
1215
except ImportError:
1316
wcwidth = None
1417
HAS_WCWIDTH = False
1518
HAS_WCWIDTH_030 = False
19+
HAS_WCWIDTH_WIDTH = False
1620

1721
requires_wcwidth = pytest.mark.skipif(not HAS_WCWIDTH, reason="requires wcwidth")
1822

1923
requires_wcwidth_030 = pytest.mark.skipif(not HAS_WCWIDTH_030, reason="requires wcwidth >= 0.3.0")
2024

25+
requires_wcwidth_width = pytest.mark.skipif(
26+
not HAS_WCWIDTH_WIDTH, reason="requires wcwidth with width() API"
27+
)
28+
2129

2230
class TestGraphemeClusterWidth:
2331
"""Tests for correct width calculation of grapheme clusters."""
@@ -237,3 +245,38 @@ def test_ansi_colored_flag_wrap(self):
237245
lines = [line.strip() for line in result.split("\n") if line.strip()]
238246
flag_parts_same_line = any("\U0001f1fa" in line and "\U0001f1f8" in line for line in lines)
239247
assert flag_parts_same_line
248+
249+
250+
class TestVisibleWidthFallback:
251+
"""Tests for _visible_width wcwidth version compatibility.
252+
253+
Covers both the modern wcwidth.width() path (>= 0.3.0) and the legacy
254+
wcswidth() path used when width() is not available.
255+
"""
256+
257+
@requires_wcwidth_width
258+
def test_visible_width_new_api_strips_ansi(self):
259+
"""_visible_width returns correct width via wcwidth.width() with ANSI codes."""
260+
from tabulate import _visible_width
261+
262+
# Two Korean chars (each 2 cols wide) wrapped in ANSI color codes.
263+
# wcwidth.width() handles ANSI internally, so no explicit stripping needed.
264+
colored_wide = "\x1b[31m한글\x1b[0m"
265+
assert _visible_width(colored_wide) == 4
266+
267+
@requires_wcwidth
268+
def test_visible_width_legacy_api_strips_ansi(self):
269+
"""_visible_width strips ANSI before wcswidth() when width() is unavailable."""
270+
import tabulate as tabulate_module
271+
from tabulate import _visible_width
272+
273+
# Build a mock wcwidth that exposes only wcswidth(), not width().
274+
# spec= limits auto-created attributes, so hasattr(mock, "width") is False.
275+
legacy_wcwidth = mock.MagicMock(spec=["wcswidth"])
276+
legacy_wcwidth.wcswidth.side_effect = wcwidth.wcswidth
277+
278+
colored_wide = "\x1b[31m한글\x1b[0m"
279+
with mock.patch.object(tabulate_module, "wcwidth", legacy_wcwidth):
280+
result = _visible_width(colored_wide)
281+
282+
assert result == 4

0 commit comments

Comments
 (0)