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 .exceptions import (
@@ -44,6 +46,7 @@ def __init__(
44
46
self .__first_col_heading = options .first_col_heading
45
47
self .__last_col_heading = options .last_col_heading
46
48
self .__cell_padding = options .cell_padding
49
+ self .__use_wcwidth = options .use_wcwidth
47
50
48
51
# calculate number of columns
49
52
self .__columns = self .__count_columns ()
@@ -93,7 +96,7 @@ def __auto_column_widths(self) -> list[int]:
93
96
def widest_line (value : SupportsStr ) -> int :
94
97
"""Returns the width of the longest line in a multi-line string"""
95
98
text = str (value )
96
- return max (len (line ) for line in text .splitlines ()) if len (text ) else 0
99
+ return max (self . __str_width (line ) for line in text .splitlines ()) if len (text ) else 0
97
100
98
101
column_widths = []
99
102
# get the width necessary for each column
@@ -145,17 +148,18 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
145
148
text = str (cell_value )
146
149
padding = " " * self .__cell_padding
147
150
padded_text = f"{ padding } { text } { padding } "
151
+ text_width = self .__str_width (padded_text )
148
152
if alignment == Alignment .LEFT :
149
153
# pad with spaces on the end
150
- return padded_text + (" " * (width - len ( padded_text ) ))
154
+ return padded_text + (" " * (width - text_width ))
151
155
if alignment == Alignment .CENTER :
152
156
# pad with spaces, half on each side
153
- before = " " * floor ((width - len ( padded_text ) ) / 2 )
154
- after = " " * ceil ((width - len ( padded_text ) ) / 2 )
157
+ before = " " * floor ((width - text_width ) / 2 )
158
+ after = " " * ceil ((width - text_width ) / 2 )
155
159
return before + padded_text + after
156
160
if alignment == Alignment .RIGHT :
157
161
# pad with spaces at the beginning
158
- return (" " * (width - len ( padded_text ) )) + padded_text
162
+ return (" " * (width - text_width )) + padded_text
159
163
raise InvalidAlignmentError (alignment )
160
164
161
165
def __row_to_ascii (
@@ -344,6 +348,23 @@ def __body_to_ascii(self, body: list[list[SupportsStr]]) -> str:
344
348
for row in body
345
349
)
346
350
351
+ def __str_width (self , text : str ) -> int :
352
+ """
353
+ Returns the width of the string in characters for the purposes of monospace
354
+ formatting. This is usually the same as the length of the string, but can be
355
+ different for double-width characters (East Asian Wide and East Asian Fullwidth)
356
+ or zero-width characters (combining characters, zero-width space, etc.)
357
+
358
+ Args:
359
+ text: The text to measure
360
+
361
+ Returns:
362
+ The width of the string in characters
363
+ """
364
+ width = wcswidth (text ) if self .__use_wcwidth else - 1
365
+ # if use_wcwidth is False or wcswidth fails, fall back to len
366
+ return width if width >= 0 else len (text )
367
+
347
368
def to_ascii (self ) -> str :
348
369
"""Generates a formatted ASCII table
349
370
@@ -380,9 +401,13 @@ def table2ascii(
380
401
alignments : list [Alignment ] | None = None ,
381
402
cell_padding : int = 1 ,
382
403
style : TableStyle = PresetStyle .double_thin_compact ,
404
+ use_wcwidth : bool = True ,
383
405
) -> str :
384
406
"""Convert a 2D Python table to ASCII text
385
407
408
+ .. versionchanged:: 1.0.0
409
+ Added the ``use_wcwidth`` parameter defaulting to :py:obj:`True`.
410
+
386
411
Args:
387
412
header: List of column values in the table's header row. All values should be :class:`str`
388
413
or support :class:`str` conversion. If not specified, the table will not have a header row.
@@ -396,7 +421,7 @@ def table2ascii(
396
421
Defaults to :py:obj:`False`.
397
422
column_widths: List of widths in characters for each column. Any value of :py:obj:`None`
398
423
indicates that the column width should be determined automatically. If :py:obj:`None`
399
- is passed instead of a :py:obj:`~typing.List `, all columns will be automatically sized.
424
+ is passed instead of a :class:`list `, all columns will be automatically sized.
400
425
Defaults to :py:obj:`None`.
401
426
alignments: List of alignments for each column
402
427
(ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to
@@ -406,6 +431,11 @@ def table2ascii(
406
431
Defaults to ``1``.
407
432
style: Table style to use for styling (preset styles can be imported).
408
433
Defaults to :ref:`PresetStyle.double_thin_compact <PresetStyle.double_thin_compact>`.
434
+ use_wcwidth: Whether to use :func:`wcwidth.wcswidth` to determine the width of each cell instead of
435
+ :func:`len`. The :func:`~wcwidth.wcswidth` function takes into account double-width characters
436
+ (East Asian Wide and East Asian Fullwidth) and zero-width characters (combining characters,
437
+ zero-width space, etc.), whereas :func:`len` determines the width solely based on the number of
438
+ characters in the string. Defaults to :py:obj:`True`.
409
439
410
440
Returns:
411
441
The generated ASCII table
@@ -421,5 +451,6 @@ def table2ascii(
421
451
alignments = alignments ,
422
452
cell_padding = cell_padding ,
423
453
style = style ,
454
+ use_wcwidth = use_wcwidth ,
424
455
),
425
456
).to_ascii ()
0 commit comments