2
2
3
3
from math import ceil , floor
4
4
5
+ from wcwidth import wcswidth
6
+
5
7
from .alignment import Alignment
6
8
from .annotations import SupportsStr
7
9
from .options import Options
@@ -36,6 +38,7 @@ def __init__(
36
38
self .__first_col_heading = options .first_col_heading
37
39
self .__last_col_heading = options .last_col_heading
38
40
self .__cell_padding = options .cell_padding
41
+ self .__use_wcwidth = options .use_wcwidth
39
42
40
43
# calculate number of columns
41
44
self .__columns = self .__count_columns ()
@@ -105,7 +108,7 @@ def __auto_column_widths(self) -> list[int]:
105
108
def widest_line (value : SupportsStr ) -> int :
106
109
"""Returns the width of the longest line in a multi-line string"""
107
110
text = str (value )
108
- return max (len (line ) for line in text .splitlines ()) if len (text ) else 0
111
+ return max (self . __str_width (line ) for line in text .splitlines ()) if len (text ) else 0
109
112
110
113
column_widths = []
111
114
# get the width necessary for each column
@@ -133,17 +136,18 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
133
136
text = str (cell_value )
134
137
padding = " " * self .__cell_padding
135
138
padded_text = f"{ padding } { text } { padding } "
139
+ text_width = self .__str_width (padded_text )
136
140
if alignment == Alignment .LEFT :
137
141
# pad with spaces on the end
138
- return padded_text + (" " * (width - len ( padded_text ) ))
142
+ return padded_text + (" " * (width - text_width ))
139
143
if alignment == Alignment .CENTER :
140
144
# pad with spaces, half on each side
141
- before = " " * floor ((width - len ( padded_text ) ) / 2 )
142
- after = " " * ceil ((width - len ( padded_text ) ) / 2 )
145
+ before = " " * floor ((width - text_width ) / 2 )
146
+ after = " " * ceil ((width - text_width ) / 2 )
143
147
return before + padded_text + after
144
148
if alignment == Alignment .RIGHT :
145
149
# pad with spaces at the beginning
146
- return (" " * (width - len ( padded_text ) )) + padded_text
150
+ return (" " * (width - text_width )) + padded_text
147
151
raise ValueError (f"The value '{ alignment } ' is not valid for alignment." )
148
152
149
153
def __row_to_ascii (
@@ -291,6 +295,23 @@ def __body_to_ascii(self, body: list[list[SupportsStr]]) -> str:
291
295
for row in body
292
296
)
293
297
298
+ def __str_width (self , text : str ) -> int :
299
+ """
300
+ Returns the width of the string in characters for the purposes of monospace
301
+ formatting. This is usually the same as the length of the string, but can be
302
+ different for double-width characters (East Asian Wide and East Asian Fullwidth)
303
+ or zero-width characters (combining characters, zero-width space, etc.)
304
+
305
+ Args:
306
+ text: The text to measure
307
+
308
+ Returns:
309
+ The width of the string in characters
310
+ """
311
+ width = wcswidth (text ) if self .__use_wcwidth else - 1
312
+ # if use_wcwidth is False or wcswidth fails, fall back to len
313
+ return width if width >= 0 else len (text )
314
+
294
315
def to_ascii (self ) -> str :
295
316
"""
296
317
Generates a formatted ASCII table
@@ -328,6 +349,7 @@ def table2ascii(
328
349
alignments : list [Alignment ] | None = None ,
329
350
cell_padding : int = 1 ,
330
351
style : TableStyle = PresetStyle .double_thin_compact ,
352
+ use_wcwidth : bool = False ,
331
353
) -> str :
332
354
"""
333
355
Convert a 2D Python table to ASCII text
@@ -345,7 +367,7 @@ def table2ascii(
345
367
Defaults to :py:obj:`False`.
346
368
column_widths: List of widths in characters for each column. Any value of :py:obj:`None`
347
369
indicates that the column width should be determined automatically. If :py:obj:`None`
348
- is passed instead of a :py:obj:`~typing.List `, all columns will be automatically sized.
370
+ is passed instead of a :class:`list `, all columns will be automatically sized.
349
371
Defaults to :py:obj:`None`.
350
372
alignments: List of alignments for each column
351
373
(ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to
@@ -355,6 +377,11 @@ def table2ascii(
355
377
Defaults to ``1``.
356
378
style: Table style to use for styling (preset styles can be imported).
357
379
Defaults to :ref:`PresetStyle.double_thin_compact <PresetStyle.double_thin_compact>`.
380
+ use_wcwidth: Whether to use :func:`wcwidth.wcswidth` to determine the width of each cell instead of
381
+ :func:`len`. This is useful when dealing with double-width characters
382
+ (East Asian Wide and East Asian Fullwidth) or zero-width characters
383
+ (combining characters, zero-width space, etc.) which are not properly handled by :func:`len`.
384
+ Defaults to :py:obj:`False`.
358
385
359
386
Returns:
360
387
The generated ASCII table
@@ -370,5 +397,6 @@ def table2ascii(
370
397
alignments = alignments ,
371
398
cell_padding = cell_padding ,
372
399
style = style ,
400
+ use_wcwidth = use_wcwidth ,
373
401
),
374
402
).to_ascii ()
0 commit comments