Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dynamic = ["version"]
Homepage = "https://github.com/astanin/python-tabulate"

[project.optional-dependencies]
widechars = ["wcwidth"]
widechars = ["wcwidth>=0.5.1"]

[project.scripts]
tabulate = "tabulate:_main"
Expand Down
66 changes: 56 additions & 10 deletions tabulate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,9 @@ def _visible_width(s):
# optional wide-character support
if wcwidth is not None and WIDE_CHARS_MODE:
len_fn = wcwidth.wcswidth
if hasattr(wcwidth, "width"):
# wcwidth >=0.3.0 handles ansi
return wcwidth.width(s)
else:
len_fn = len
if isinstance(s, (str, bytes)):
Expand Down Expand Up @@ -1588,9 +1591,11 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
if headers == "keys":
headers = field_names
rows = [
[getattr(row, f) for f in field_names]
if not _is_separating_line(row)
else row
(
[getattr(row, f) for f in field_names]
if not _is_separating_line(row)
else row
)
for row in rows
]

Expand Down Expand Up @@ -1638,7 +1643,13 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
return rows, headers, headers_pad


def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True, break_long_words=_BREAK_LONG_WORDS, break_on_hyphens=_BREAK_ON_HYPHENS):
def _wrap_text_to_colwidths(
list_of_lists,
colwidths,
numparses=True,
break_long_words=_BREAK_LONG_WORDS,
break_on_hyphens=_BREAK_ON_HYPHENS,
):
if len(list_of_lists):
num_cols = len(list_of_lists[0])
else:
Expand All @@ -1655,10 +1666,15 @@ def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True, break_long
continue

if width is not None:
wrapper = _CustomTextWrap(width=width, break_long_words=break_long_words, break_on_hyphens=break_on_hyphens)
wrapper_wrap = partial(
_wrap_text,
width=width,
break_long_words=break_long_words,
break_on_hyphens=break_on_hyphens,
)
casted_cell = str(cell)
wrapped = [
"\n".join(wrapper.wrap(line))
"\n".join(wrapper_wrap(line))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functools.partial() was just used to help make this change clear and concise, not needed but doesn't hurt and already imported and used somewhere else

for line in casted_cell.splitlines()
if line.strip() != ""
]
Expand Down Expand Up @@ -2258,7 +2274,11 @@ def tabulate(

numparses = _expand_numparse(disable_numparse, num_cols)
list_of_lists = _wrap_text_to_colwidths(
list_of_lists, maxcolwidths, numparses=numparses, break_long_words=break_long_words, break_on_hyphens=break_on_hyphens
list_of_lists,
maxcolwidths,
numparses=numparses,
break_long_words=break_long_words,
break_on_hyphens=break_on_hyphens,
)

if maxheadercolwidths is not None:
Expand All @@ -2272,7 +2292,11 @@ def tabulate(

numparses = _expand_numparse(disable_numparse, num_cols)
headers = _wrap_text_to_colwidths(
[headers], maxheadercolwidths, numparses=numparses, break_long_words=break_long_words, break_on_hyphens=break_on_hyphens
[headers],
maxheadercolwidths,
numparses=numparses,
break_long_words=break_long_words,
break_on_hyphens=break_on_hyphens,
)[0]

# empty values in the first column of RST tables should be escaped (issue #82)
Expand Down Expand Up @@ -2672,6 +2696,26 @@ def _format_table(
return ""


def _wrap_text(text, width, break_long_words=True, break_on_hyphens=True):
"""Wrap text to width with wide character and ANSI code support."""
# wcwidth >= 0.5.0 has wrap() with proper grapheme cluster support and
# propagate_sgr=True by default, which handles ANSI code propagation natively.
if wcwidth is not None and hasattr(wcwidth, "wrap"):
return wcwidth.wrap(
text,
width,
break_long_words=break_long_words,
break_on_hyphens=break_on_hyphens,
)
else:
# Fallback for wcwidth < 0.3.0 or no wcwidth
return _CustomTextWrap(
width=width,
break_long_words=break_long_words,
break_on_hyphens=break_on_hyphens,
).wrap(text)


class _CustomTextWrap(textwrap.TextWrapper):
"""A custom implementation of CPython's textwrap.TextWrapper. This supports
both wide characters (Korea, Japanese, Chinese) - including mixed string.
Expand Down Expand Up @@ -2740,11 +2784,13 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
# of the next chunk onto the current line as will fit.
if self.break_long_words:
# Tabulate Custom: Build the string up piece-by-piece in order to
# take each charcter's width into account
# take each character's width into account
chunk = reversed_chunks[-1]
i = 1
# Only count printable characters, so strip_ansi first, index later.
while len(_strip_ansi(chunk)[:i]) <= space_left:
# Use self._len() instead of len() to account for displayed width, eg.
# wide chars like CJK count as 2 when using wcwidth<0.3.0 without wrap()
while self._len(_strip_ansi(chunk)[:i]) <= space_left:
i = i + 1
# Consider escape codes when breaking words up
total_escape_len = 0
Expand Down
5 changes: 1 addition & 4 deletions test/test_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"""API properties.

"""
"""API properties."""

from tabulate import tabulate, tabulate_formats, simple_separated_format
from common import skip


try:
from inspect import signature, _empty
except ImportError:
Expand Down
5 changes: 1 addition & 4 deletions test/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Command-line interface.

"""
"""Command-line interface."""

import os
import sys
Expand All @@ -12,7 +10,6 @@

from common import assert_equal


SAMPLE_SIMPLE_FORMAT = "\n".join(
[
"----- ------ -------------",
Expand Down
Loading