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
@@ -35,6 +37,7 @@ def __init__(
35
37
self .__first_col_heading = options .first_col_heading
36
38
self .__last_col_heading = options .last_col_heading
37
39
self .__cell_padding = options .cell_padding
40
+ self .__use_wcwidth = options .use_wcwidth
38
41
39
42
# calculate number of columns
40
43
self .__columns = self .__count_columns ()
@@ -86,7 +89,7 @@ def __auto_column_widths(self) -> list[int]:
86
89
def widest_line (value : SupportsStr ) -> int :
87
90
"""Returns the width of the longest line in a multi-line string"""
88
91
text = str (value )
89
- return max (len (line ) for line in text .splitlines ()) if len (text ) else 0
92
+ return max (self . __str_width (line ) for line in text .splitlines ()) if len (text ) else 0
90
93
91
94
column_widths = []
92
95
# get the width necessary for each column
@@ -140,17 +143,18 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
140
143
text = str (cell_value )
141
144
padding = " " * self .__cell_padding
142
145
padded_text = f"{ padding } { text } { padding } "
146
+ text_width = self .__str_width (padded_text )
143
147
if alignment == Alignment .LEFT :
144
148
# pad with spaces on the end
145
- return padded_text + (" " * (width - len ( padded_text ) ))
149
+ return padded_text + (" " * (width - text_width ))
146
150
if alignment == Alignment .CENTER :
147
151
# pad with spaces, half on each side
148
- before = " " * floor ((width - len ( padded_text ) ) / 2 )
149
- after = " " * ceil ((width - len ( padded_text ) ) / 2 )
152
+ before = " " * floor ((width - text_width ) / 2 )
153
+ after = " " * ceil ((width - text_width ) / 2 )
150
154
return before + padded_text + after
151
155
if alignment == Alignment .RIGHT :
152
156
# pad with spaces at the beginning
153
- return (" " * (width - len ( padded_text ) )) + padded_text
157
+ return (" " * (width - text_width )) + padded_text
154
158
raise ValueError (f"The value '{ alignment } ' is not valid for alignment." )
155
159
156
160
def __row_to_ascii (
@@ -339,6 +343,23 @@ def __body_to_ascii(self, body: list[list[SupportsStr]]) -> str:
339
343
for row in body
340
344
)
341
345
346
+ def __str_width (self , text : str ) -> int :
347
+ """
348
+ Returns the width of the string in characters for the purposes of monospace
349
+ formatting. This is usually the same as the length of the string, but can be
350
+ different for double-width characters (East Asian Wide and East Asian Fullwidth)
351
+ or zero-width characters (combining characters, zero-width space, etc.)
352
+
353
+ Args:
354
+ text: The text to measure
355
+
356
+ Returns:
357
+ The width of the string in characters
358
+ """
359
+ width = wcswidth (text ) if self .__use_wcwidth else - 1
360
+ # if use_wcwidth is False or wcswidth fails, fall back to len
361
+ return width if width >= 0 else len (text )
362
+
342
363
def to_ascii (self ) -> str :
343
364
"""Generates a formatted ASCII table
344
365
@@ -375,6 +396,7 @@ def table2ascii(
375
396
alignments : list [Alignment ] | None = None ,
376
397
cell_padding : int = 1 ,
377
398
style : TableStyle = PresetStyle .double_thin_compact ,
399
+ use_wcwidth : bool = False ,
378
400
) -> str :
379
401
"""Convert a 2D Python table to ASCII text
380
402
@@ -391,7 +413,7 @@ def table2ascii(
391
413
Defaults to :py:obj:`False`.
392
414
column_widths: List of widths in characters for each column. Any value of :py:obj:`None`
393
415
indicates that the column width should be determined automatically. If :py:obj:`None`
394
- is passed instead of a :py:obj:`~typing.List `, all columns will be automatically sized.
416
+ is passed instead of a :class:`list `, all columns will be automatically sized.
395
417
Defaults to :py:obj:`None`.
396
418
alignments: List of alignments for each column
397
419
(ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to
@@ -401,6 +423,11 @@ def table2ascii(
401
423
Defaults to ``1``.
402
424
style: Table style to use for styling (preset styles can be imported).
403
425
Defaults to :ref:`PresetStyle.double_thin_compact <PresetStyle.double_thin_compact>`.
426
+ use_wcwidth: Whether to use :func:`wcwidth.wcswidth` to determine the width of each cell instead of
427
+ :func:`len`. This is useful when dealing with double-width characters
428
+ (East Asian Wide and East Asian Fullwidth) or zero-width characters
429
+ (combining characters, zero-width space, etc.) which are not properly handled by :func:`len`.
430
+ Defaults to :py:obj:`False`.
404
431
405
432
Returns:
406
433
The generated ASCII table
@@ -416,5 +443,6 @@ def table2ascii(
416
443
alignments = alignments ,
417
444
cell_padding = cell_padding ,
418
445
style = style ,
446
+ use_wcwidth = use_wcwidth ,
419
447
),
420
448
).to_ascii ()
0 commit comments