|
1 | 1 | """Tests for Unicode grapheme cluster handling in tabulate.""" |
2 | 2 |
|
| 3 | +import unittest.mock as mock |
| 4 | + |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | from tabulate import tabulate |
|
9 | 11 |
|
10 | 12 | HAS_WCWIDTH = True |
11 | 13 | HAS_WCWIDTH_030 = hasattr(wcwidth, "wrap") |
| 14 | + HAS_WCWIDTH_WIDTH = hasattr(wcwidth, "width") |
12 | 15 | except ImportError: |
13 | 16 | wcwidth = None |
14 | 17 | HAS_WCWIDTH = False |
15 | 18 | HAS_WCWIDTH_030 = False |
| 19 | + HAS_WCWIDTH_WIDTH = False |
16 | 20 |
|
17 | 21 | requires_wcwidth = pytest.mark.skipif(not HAS_WCWIDTH, reason="requires wcwidth") |
18 | 22 |
|
19 | 23 | requires_wcwidth_030 = pytest.mark.skipif(not HAS_WCWIDTH_030, reason="requires wcwidth >= 0.3.0") |
20 | 24 |
|
| 25 | +requires_wcwidth_width = pytest.mark.skipif( |
| 26 | + not HAS_WCWIDTH_WIDTH, reason="requires wcwidth with width() API" |
| 27 | +) |
| 28 | + |
21 | 29 |
|
22 | 30 | class TestGraphemeClusterWidth: |
23 | 31 | """Tests for correct width calculation of grapheme clusters.""" |
@@ -237,3 +245,38 @@ def test_ansi_colored_flag_wrap(self): |
237 | 245 | lines = [line.strip() for line in result.split("\n") if line.strip()] |
238 | 246 | flag_parts_same_line = any("\U0001f1fa" in line and "\U0001f1f8" in line for line in lines) |
239 | 247 | 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