Skip to content

Commit 3eb1f35

Browse files
committed
add tests for usage without wcwidth library
1 parent 5313627 commit 3eb1f35

File tree

2 files changed

+175
-3
lines changed

2 files changed

+175
-3
lines changed

tabulate/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717

1818
try:
1919
import wcwidth # optional wide-character (CJK) support
20-
except ImportError:
20+
except ImportError: # pragma: no cover
2121
wcwidth = None
2222

2323
try:
2424
__version__ = version("tabulate") # installed package
25-
except PackageNotFoundError:
25+
except PackageNotFoundError: # pragma: no cover
2626
try:
2727
from ._version import version as __version__ # editable / source checkout
2828
except ImportError:
@@ -2894,7 +2894,7 @@ def _wrap_chunks(self, chunks):
28942894
return lines
28952895

28962896

2897-
if __name__ == "__main__":
2897+
if __name__ == "__main__": # pragma: no cover
28982898
from .cli import _main
28992899

29002900
_main()

test/test_no_wcwidth.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Tests for code paths executed when the wcwidth library is not available.
2+
3+
These tests mock wcwidth as None to cover branches that would otherwise only
4+
run in environments without wcwidth installed.
5+
"""
6+
7+
from unittest.mock import patch
8+
9+
import tabulate as T
10+
from tabulate import tabulate
11+
12+
from common import assert_equal
13+
14+
15+
def _patch_no_wcwidth():
16+
"""Return a context manager that simulates wcwidth being unavailable."""
17+
return (
18+
patch.object(T, "wcwidth", None),
19+
patch.object(T, "WIDE_CHARS_MODE", False),
20+
)
21+
22+
23+
# ---------------------------------------------------------------------------
24+
# _visible_width() fallback paths
25+
# ---------------------------------------------------------------------------
26+
27+
28+
def test_visible_width_str_no_wcwidth():
29+
"Internal: _visible_width() falls back to len() for str when wcwidth is None"
30+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
31+
assert T._visible_width("hello") == 5
32+
# ANSI codes must still be stripped
33+
assert T._visible_width("\x1b[31mhello\x1b[0m") == 5
34+
# Wide chars are counted as 1 each (no wcwidth)
35+
assert T._visible_width("配列") == 2
36+
37+
38+
def test_visible_width_bytes_no_wcwidth():
39+
"Internal: _visible_width() falls back to len() for bytes when wcwidth is None"
40+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
41+
assert T._visible_width(b"hello") == 5
42+
43+
44+
def test_visible_width_non_string_no_wcwidth():
45+
"Internal: _visible_width() falls back to len(str(...)) for non-strings when wcwidth is None"
46+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
47+
assert T._visible_width(12345) == 5
48+
assert T._visible_width(3.14) == 4
49+
50+
51+
# ---------------------------------------------------------------------------
52+
# _choose_width_fn() and _align_column_choose_width_fn() fallback paths
53+
# ---------------------------------------------------------------------------
54+
55+
56+
def test_choose_width_fn_no_wcwidth_no_invisible():
57+
"Internal: _choose_width_fn() returns len when wcwidth is None and no invisible chars"
58+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
59+
fn = T._choose_width_fn(has_invisible=False, enable_widechars=False, is_multiline=False)
60+
assert fn is len
61+
62+
63+
def test_choose_width_fn_no_wcwidth_multiline():
64+
"Internal: _choose_width_fn() wraps len in multiline handler when wcwidth is None"
65+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
66+
fn = T._choose_width_fn(has_invisible=False, enable_widechars=False, is_multiline=True)
67+
# The result is a lambda, not len directly, but it should compute max line length
68+
assert fn("foo\nbarbaz") == 6
69+
70+
71+
def test_align_column_choose_width_fn_no_wcwidth():
72+
"Internal: _align_column_choose_width_fn() returns len when wcwidth is None"
73+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
74+
fn = T._align_column_choose_width_fn(
75+
has_invisible=False, enable_widechars=False, is_multiline=False
76+
)
77+
assert fn is len
78+
79+
80+
def test_align_column_choose_width_fn_no_wcwidth_multiline():
81+
"Internal: _align_column_choose_width_fn() returns per-line widths list for multiline when wcwidth is None"
82+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
83+
fn = T._align_column_choose_width_fn(
84+
has_invisible=False, enable_widechars=False, is_multiline=True
85+
)
86+
# _align_column_multiline_width returns a list of widths per line
87+
assert fn("foo\nbarbaz") == [3, 6]
88+
89+
90+
# ---------------------------------------------------------------------------
91+
# _CustomTextWrap._len() fallback path
92+
# ---------------------------------------------------------------------------
93+
94+
95+
def test_textwrapper_len_no_wcwidth():
96+
"Internal: _CustomTextWrap._len() falls back to len() when wcwidth is None"
97+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
98+
assert T._CustomTextWrap._len("hello") == 5
99+
assert T._CustomTextWrap._len("\x1b[31mhello\x1b[0m") == 5
100+
101+
102+
# ---------------------------------------------------------------------------
103+
# End-to-end tabulate() with wide characters, no wcwidth
104+
# ---------------------------------------------------------------------------
105+
106+
107+
def test_tabulate_wide_chars_no_wcwidth_grid():
108+
"Output: grid with wide characters treats them as width-1 when wcwidth is None"
109+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
110+
table = [["spam", 41.9999], ["eggs", "451.0"]]
111+
headers = ["strings", "配列"]
112+
result = tabulate(table, headers, tablefmt="grid")
113+
# With no wcwidth, "配列" is treated as 2 chars wide (not 4),
114+
# so column width matches len("配列") == 2, padded to fit content.
115+
# We only assert the result is a non-empty string and doesn't crash;
116+
# the exact layout depends on len()-based widths.
117+
assert len(result) > 0
118+
assert "配列" in result
119+
assert "spam" in result
120+
expected = "\n".join(
121+
[
122+
"+-----------+----------+",
123+
"| strings | 配列 |",
124+
"+===========+==========+",
125+
"| spam | 41.9999 |",
126+
"+-----------+----------+",
127+
"| eggs | 451 |",
128+
"+-----------+----------+",
129+
]
130+
)
131+
assert_equal(expected.splitlines(), result.splitlines())
132+
133+
134+
def test_tabulate_wide_chars_no_wcwidth_plain():
135+
"Output: plain with wide characters uses len() when wcwidth is None"
136+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
137+
table = [["привет", 1], ["你好", 2]]
138+
result = tabulate(table, tablefmt="plain")
139+
assert "привет" in result
140+
assert "你好" in result
141+
142+
143+
def test_tabulate_wide_chars_no_wcwidth_simple_grid():
144+
"Output: simple_grid with wide characters uses len() when wcwidth is None"
145+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
146+
table = [["가나", "abc"], ["de", "fgh"]]
147+
result = tabulate(table, tablefmt="simple_grid")
148+
assert "가나" in result
149+
assert len(result) > 0
150+
151+
152+
# ---------------------------------------------------------------------------
153+
# maxcolwidths path through CustomTextWrapper when wcwidth is None
154+
# ---------------------------------------------------------------------------
155+
156+
157+
def test_maxcolwidths_no_wcwidth():
158+
"Output: maxcolwidths autowrap uses len() when wcwidth is None"
159+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
160+
table = [["hdr", "fold"], ["1", "very long data"]]
161+
expected = "\n".join([" hdr fold", " 1 very long", " data"])
162+
result = tabulate(table, headers="firstrow", tablefmt="plain", maxcolwidths=[10, 10])
163+
assert_equal(expected, result)
164+
165+
166+
def test_maxcolwidths_wide_chars_no_wcwidth():
167+
"Output: maxcolwidths with wide chars wraps by byte-len when wcwidth is None"
168+
with patch.object(T, "wcwidth", None), patch.object(T, "WIDE_CHARS_MODE", False):
169+
table = [["hdr", "fold"], ["1", "약간 감싸면 더 잘 보일 수있는 긴 설명"]]
170+
result = tabulate(table, headers="firstrow", tablefmt="plain", maxcolwidths=[10, 10])
171+
assert "hdr" in result
172+
assert len(result) > 0

0 commit comments

Comments
 (0)