Add title_cb for truncating titles.
Opened this issue · 3 comments
Robpol86 commented
The default is to hide the title if it doesn't fit. I didn't remember this until I was done writing truncating code. Saving it here for future reference.
def build_border(column_widths, horizontal, left, intersect, right, title=None):
"""Build the top/bottom/middle row. Optionally embed the table title within the border.
Title is truncated to fit in between left/right characters.
Example return value:
('<', '-----', '+', '------', '+', '-------', '>')
('<', 'My Table', '----', '+', '------->')
:param iter column_widths: List of integers representing column widths.
:param str horizontal: Character to stretch across each column.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str title: Overlay the title on the border between the left and right characters.
:return: Prepared border as a tuple of strings.
:rtype: tuple
"""
if not title or not column_widths or not horizontal:
return tuple(combine((horizontal * c for c in column_widths), left, intersect, right))
title, length = truncate(title, sum(column_widths) + len(intersect) * (len(column_widths) - 1))
# Handle title fitting in the first column.
if length == column_widths[0]:
return tuple(combine([title] + [horizontal * c for c in column_widths[1:]], left, intersect, right))
if length < column_widths[0]:
columns = [title + horizontal * (column_widths[0] - length)] + [horizontal * c for c in column_widths[1:]]
return tuple(combine(columns, left, intersect, right))
# Handle wide titles/narrow columns.
columns_and_intersects = [title]
for width in combine(column_widths, None, bool(intersect), None):
# If title is taken care of.
if length < 1:
columns_and_intersects.append(intersect if width is True else horizontal * width)
# If title's last character overrides an intersect character.
elif width is True and length == 1:
length = 0
# If this is an intersect character that is overridden by the title.
elif width is True:
length -= 1
# If title's last character is within a column.
elif width >= length:
columns_and_intersects[0] += horizontal * (width - length) # Append horizontal chars to title.
length = 0
# If remainder of title won't fit in a column.
else:
length -= width
return tuple(combine(columns_and_intersects, left, None, right))
def truncate(string, max_length):
"""Truncate string to a maximum length. Handles CJK characters.
:param str string: String to operate on.
:param int max_length: Truncate string to this visible size. May truncate to one shorter if CJK in the middle.
:return: Truncated string and its length (str, int).
:rtype: tuple
"""
truncated = list()
length = 0
done = False
# Convert to unicode.
try:
string = string.decode('u8')
except (AttributeError, UnicodeEncodeError):
pass
for item in RE_COLOR_ANSI.split(string):
if not item:
continue
if RE_COLOR_ANSI.match(item):
truncated.append(item)
continue
if done:
continue
for char in item:
width = 2 if unicodedata.east_asian_width(char) in ('F', 'W') else 1
if length + width > max_length:
done = True
break
truncated.append(char)
length += width
return ''.join(truncated), length
Robpol86 commented
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore
from colorclass import Color
from termcolor import colored
from terminaltables.width_and_alignment import truncate
@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
('TEST', 2, 'TE', 2),
('TEST', 0, '', 0),
('TEST', 5, 'TEST', 4),
('', 2, '', 0),
('', 0, '', 0),
('', 6, '', 0),
])
def test_ascii(string, max_length, expected_str, expected_len):
"""Test with ASCII characters only.
:param str string: String to operate on.
:param int max_length: Truncate to this size.
:param str expected_str: Expected truncated string.
:param int expected_len: Expected truncated string size.
"""
actual_str, actual_len = truncate(string, max_length)
assert actual_str == expected_str
assert actual_len == expected_len
@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
('世界你好', 8, u'世界你好', 8),
('世界你好', 4, u'世界', 4),
('世界你好', 3, u'世', 2),
('a世界你好', 3, u'a世', 3),
('שלום', 4, u'שלום', 4),
('שלום', 3, u'שלו', 3),
('معرب', 4, u'معرب', 4),
('معرب', 3, u'معر', 3),
])
def test_cjk_rtl(string, max_length, expected_str, expected_len):
"""Test with CJK and RTL characters.
:param str string: String to operate on.
:param int max_length: Truncate to this size.
:param str expected_str: Expected truncated string.
:param int expected_len: Expected truncated string size.
"""
actual_str, actual_len = truncate(string, max_length)
assert actual_str == expected_str
assert actual_len == expected_len
@pytest.mark.parametrize('string,max_length,expected_str,expected_len', [
# str+ansi
('\x1b[34mTEST\x1b[39m', 4, '\x1b[34mTEST\x1b[39m', 4),
('\x1b[34mTEST\x1b[39m', 2, '\x1b[34mTE\x1b[39m', 2),
('\x1b[34mT\x1b[35mE\x1b[36mS\x1b[37mT\x1b[39m', 2, '\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m', 2),
('\x1b[34m世界\x1b[39m', 4, u'\x1b[34m世界\x1b[39m', 4),
('\x1b[34m世界\x1b[39m', 2, u'\x1b[34m世\x1b[39m', 2),
('\x1b[34m世\x1b[35m界\x1b[39m', 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
('\x1b[34mمعرب\x1b[39m', 4, u'\x1b[34mمعرب\x1b[39m', 4),
('\x1b[34mمعرب\x1b[39m', 2, u'\x1b[34mمع\x1b[39m', 2),
('\x1b[34mשלום\x1b[39m', 4, u'\x1b[34mשלום\x1b[39m', 4),
('\x1b[34mשלום\x1b[39m', 2, u'\x1b[34mשל\x1b[39m', 2),
# colorclass
(Color('{blue}TEST{/blue}'), 4, '\x1b[34mTEST\x1b[39m', 4),
(Color('{blue}TEST{/blue}'), 2, '\x1b[34mTE\x1b[39m', 2),
(Color('{blue}T{magenta}E{cyan}S{white}T{/blue}'), 2, '\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m', 2),
(Color(u'{blue}世界{/blue}'), 4, u'\x1b[34m世界\x1b[39m', 4),
(Color(u'{blue}世界{/blue}'), 2, u'\x1b[34m世\x1b[39m', 2),
(Color(u'{blue}世{magenta}界{/magenta}'), 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
(Color(u'{blue}معرب{/blue}'), 4, u'\x1b[34mمعرب\x1b[39m', 4),
(Color(u'{blue}معرب{/blue}'), 2, u'\x1b[34mمع\x1b[39m', 2),
(Color(u'{blue}שלום{/blue}'), 4, u'\x1b[34mשלום\x1b[39m', 4),
(Color(u'{blue}שלום{/blue}'), 2, u'\x1b[34mשל\x1b[39m', 2),
# colorama
(Fore.BLUE + 'TEST' + Fore.RESET, 4, '\x1b[34mTEST\x1b[39m', 4),
(Fore.BLUE + 'TEST' + Fore.RESET, 2, '\x1b[34mTE\x1b[39m', 2),
(
Fore.BLUE + 'T' + Fore.MAGENTA + 'E' + Fore.CYAN + 'S' + Fore.WHITE + 'T' + Fore.RESET,
2,
'\x1b[34mT\x1b[35mE\x1b[36m\x1b[37m\x1b[39m',
2
),
(Fore.BLUE + '世界' + Fore.RESET, 4, u'\x1b[34m世界\x1b[39m', 4),
(Fore.BLUE + '世界' + Fore.RESET, 2, u'\x1b[34m世\x1b[39m', 2),
(Fore.BLUE + '世' + Fore.MAGENTA + '界' + Fore.RESET, 2, u'\x1b[34m世\x1b[35m\x1b[39m', 2),
(Fore.BLUE + 'معرب' + Fore.RESET, 4, u'\x1b[34mمعرب\x1b[39m', 4),
(Fore.BLUE + 'معرب' + Fore.RESET, 2, u'\x1b[34mمع\x1b[39m', 2),
(Fore.BLUE + 'שלום' + Fore.RESET, 4, u'\x1b[34mשלום\x1b[39m', 4),
(Fore.BLUE + 'שלום' + Fore.RESET, 2, u'\x1b[34mשל\x1b[39m', 2),
# termcolor
(colored('TEST', 'blue'), 4, '\x1b[34mTEST\x1b[0m', 4),
(colored('TEST', 'blue'), 2, '\x1b[34mTE\x1b[0m', 2),
(
colored('T', 'blue') + colored('E', 'magenta') + colored('S', 'cyan') + colored('T', 'white'),
2,
'\x1b[34mT\x1b[0m\x1b[35mE\x1b[0m\x1b[36m\x1b[0m\x1b[37m\x1b[0m',
2
),
(colored('世界', 'blue'), 4, u'\x1b[34m世界\x1b[0m', 4),
(colored('世界', 'blue'), 2, u'\x1b[34m世\x1b[0m', 2),
(colored('世', 'blue') + colored('界', 'magenta'), 2, u'\x1b[34m世\x1b[0m\x1b[35m\x1b[0m', 2),
(colored('معرب', 'blue'), 4, u'\x1b[34mمعرب\x1b[0m', 4),
(colored('معرب', 'blue'), 2, u'\x1b[34mمع\x1b[0m', 2),
(colored('שלום', 'blue'), 4, u'\x1b[34mשלום\x1b[0m', 4),
(colored('שלום', 'blue'), 2, u'\x1b[34mשל\x1b[0m', 2),
])
def test_colors(string, max_length, expected_str, expected_len):
"""Test with color characters.
:param str string: String to operate on.
:param int max_length: Truncate to this size.
:param str expected_str: Expected truncated string.
:param int expected_len: Expected truncated string size.
"""
actual_str, actual_len = truncate(string, max_length)
assert actual_str == expected_str
assert actual_len == expected_len
Robpol86 commented
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore, Style
from colorclass import Color
from termcolor import colored
from terminaltables.build import build_border
@pytest.mark.parametrize('column_widths,horizontal,left,intersect,right,expected', [
([5, 6, 7], '-', '<', '+', '>', '<-----+------+------->'),
([1, 1, 1], '-', '', '', '', '---'),
([1, 1, 1], '', '', '', '', ''),
([1], '-', '<', '+', '>', '<->'),
([], '-', '<', '+', '>', '<>'),
])
def test_no_title(column_widths, horizontal, left, intersect, right, expected):
"""Test without title.
:param iter column_widths: List of integers representing column widths.
:param str horizontal: Character to stretch across each column.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
actual = build_border(column_widths, horizontal, left, intersect, right)
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,intersect,expected', [
([20], '+', 'Applications--------'),
([20], '', 'Applications--------'),
([15, 5], '+', 'Applications---+-----'),
([15, 5], '', 'Applications--------'),
([12], '+', 'Applications'),
([12], '', 'Applications'),
([12, 1], '+', 'Applications+-'),
([12, 1], '', 'Applications-'),
([12, 0], '+', 'Applications+'),
([12, 0], '', 'Applications'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_first_column_fit(column_widths, left, intersect, right, expected):
"""Test with title that fits in the first column.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, intersect, right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,expected', [
([20], 'Applications--------'),
([10, 10], 'Applications--------'),
([5, 5, 5, 5], 'Applications--------'),
([3, 2, 3, 2, 3, 2, 3, 2], 'Applications--------'),
([1] * 20, 'Applications--------'),
([10, 5], 'Applications---'),
([9, 5], 'Applications--'),
([8, 5], 'Applications-'),
([7, 5], 'Applications'),
([6, 5], 'Application'),
([5, 5], 'Applicatio'),
([5, 4], 'Applicati'),
([4, 4], 'Applicat'),
([4, 3], 'Applica'),
([3, 3], 'Applic'),
([3, 2], 'Appli'),
([2, 2], 'Appl'),
([2, 1], 'App'),
([1, 1], 'Ap'),
([1, 0], 'A'),
([0, 0], ''),
([1], 'A'),
([0], ''),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_no_intersect(column_widths, left, right, expected):
"""Test with no column dividers.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, '', right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,expected', [
([20], 'Applications--------'),
([0, 20], 'Applications---------'),
([20, 0], 'Applications--------+'),
([0, 0, 20], 'Applications----------'),
([20, 0, 0], 'Applications--------++'),
([10, 10], 'Applications---------'),
([11, 9], 'Applications---------'),
([12, 8], 'Applications+--------'),
([13, 7], 'Applications-+-------'),
([5, 5, 5, 5], 'Applications-----+-----'),
([4, 4, 6, 6], 'Applications----+------'),
([3, 3, 7, 7], 'Applications---+-------'),
([2, 2, 7, 9], 'Applications-+---------'),
([1, 1, 9, 9], 'Applications-+---------'),
([2, 2, 2, 2, 2, 2, 2], 'Applications--+--+--'),
([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'Applications-+-+-+-'),
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'Applications++++++++'),
([2, 2, 2, 2], 'Application'),
([1, 1, 1, 1, 1], 'Applicati'),
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'Applicati'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_intersect(column_widths, left, right, expected):
"""Test with column dividers.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, '+', right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,intersect,expected', [
([12], '+', u'蓝色--------'),
([12], '', u'蓝色--------'),
([7, 5], '+', u'蓝色---+-----'),
([7, 5], '', u'蓝色--------'),
([4], '+', u'蓝色'),
([4], '', u'蓝色'),
([4, 1], '+', u'蓝色+-'),
([4, 1], '', u'蓝色-'),
([4, 0], '+', u'蓝色+'),
([4, 0], '', u'蓝色'),
([12], '', u'蓝色--------'),
([6, 6], '', u'蓝色--------'),
([3, 3, 3, 3], '', u'蓝色--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', u'蓝色--------'),
([1] * 12, '', u'蓝色--------'),
([2, 4], '', u'蓝色--'),
([1, 4], '', u'蓝色-'),
([1, 3], '', u'蓝色'),
([1, 2], '', u'蓝-'),
([1, 1], '', u'蓝'),
([1, 0], '', '-'),
([0, 0], '', ''),
([2], '', u'蓝'),
([1], '', '-'),
([0], '', ''),
([12], '+', u'蓝色--------'),
([0, 12], '+', u'蓝色---------'),
([12, 0], '+', u'蓝色--------+'),
([0, 0, 12], '+', u'蓝色----------'),
([12, 0, 0], '+', u'蓝色--------++'),
([3, 3], '+', u'蓝色---'),
([4, 2], '+', u'蓝色+--'),
([5, 1], '+', u'蓝色-+-'),
([3, 3, 3, 3], '+', u'蓝色---+---+---'),
([2, 2, 4, 4], '+', u'蓝色-+----+----'),
([1, 1, 5, 5], '+', u'蓝色-----+-----'),
([2, 2, 2, 2], '+', u'蓝色-+--+--'),
([1, 1, 1, 1, 1], '+', u'蓝色-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', u'蓝色++'),
([1, 1], '+', u'蓝-'),
([1, 0], '+', u'蓝'),
([0, 0], '+', '+'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_cjk(column_widths, left, intersect, right, expected):
"""Test with CJK characters in title.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, intersect, right, title='蓝色')
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,intersect,expected', [
([12], '+', u'معرب--------'),
([12], '', u'معرب--------'),
([7, 5], '+', u'معرب---+-----'),
([7, 5], '', u'معرب--------'),
([4], '+', u'معرب'),
([4], '', u'معرب'),
([4, 1], '+', u'معرب+-'),
([4, 1], '', u'معرب-'),
([4, 0], '+', u'معرب+'),
([4, 0], '', u'معرب'),
([12], '', u'معرب--------'),
([6, 6], '', u'معرب--------'),
([3, 3, 3, 3], '', u'معرب--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', u'معرب--------'),
([1] * 12, '', u'معرب--------'),
([2, 4], '', u'معرب--'),
([1, 4], '', u'معرب-'),
([1, 3], '', u'معرب'),
([1, 2], '', u'معر'),
([1, 1], '', u'مع'),
([1, 0], '', u'م'),
([0, 0], '', ''),
([2], '', u'مع'),
([1], '', u'م'),
([0], '', ''),
([12], '+', u'معرب--------'),
([0, 12], '+', u'معرب---------'),
([12, 0], '+', u'معرب--------+'),
([0, 0, 12], '+', u'معرب----------'),
([12, 0, 0], '+', u'معرب--------++'),
([3, 3], '+', u'معرب---'),
([4, 2], '+', u'معرب+--'),
([5, 1], '+', u'معرب-+-'),
([3, 3, 3, 3], '+', u'معرب---+---+---'),
([2, 2, 4, 4], '+', u'معرب-+----+----'),
([1, 1, 5, 5], '+', u'معرب-----+-----'),
([2, 2, 2, 2], '+', u'معرب-+--+--'),
([1, 1, 1, 1, 1], '+', u'معرب-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', u'معرب++'),
([1, 1], '+', u'معر'),
([1, 0], '+', u'مع'),
([0, 0], '+', u'م'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_rtl(column_widths, left, intersect, right, expected):
"""Test with RTL characters in title.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, intersect, right, title='معرب')
assert ''.join(actual) == expected
@pytest.mark.parametrize('column_widths,intersect,expected', [
([12], '+', '\x1b[34mTEST\x1b[0m--------'),
([12], '', '\x1b[34mTEST\x1b[0m--------'),
([7, 5], '+', '\x1b[34mTEST\x1b[0m---+-----'),
([7, 5], '', '\x1b[34mTEST\x1b[0m--------'),
([4], '+', '\x1b[34mTEST\x1b[0m'),
([4], '', '\x1b[34mTEST\x1b[0m'),
([4, 1], '+', '\x1b[34mTEST\x1b[0m+-'),
([4, 1], '', '\x1b[34mTEST\x1b[0m-'),
([4, 0], '+', '\x1b[34mTEST\x1b[0m+'),
([4, 0], '', '\x1b[34mTEST\x1b[0m'),
([12], '', '\x1b[34mTEST\x1b[0m--------'),
([6, 6], '', '\x1b[34mTEST\x1b[0m--------'),
([3, 3, 3, 3], '', '\x1b[34mTEST\x1b[0m--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', '\x1b[34mTEST\x1b[0m--------'),
([1] * 12, '', '\x1b[34mTEST\x1b[0m--------'),
([2, 4], '', '\x1b[34mTEST\x1b[0m--'),
([1, 4], '', '\x1b[34mTEST\x1b[0m-'),
([1, 3], '', '\x1b[34mTEST\x1b[0m'),
([1, 2], '', '\x1b[34mTES\x1b[0m'),
([1, 1], '', '\x1b[34mTE\x1b[0m'),
([1, 0], '', '\x1b[34mT\x1b[0m'),
([0, 0], '', '\x1b[34m\x1b[0m'),
([2], '', '\x1b[34mTE\x1b[0m'),
([1], '', '\x1b[34mT\x1b[0m'),
([0], '', '\x1b[34m\x1b[0m'),
([12], '+', '\x1b[34mTEST\x1b[0m--------'),
([0, 12], '+', '\x1b[34mTEST\x1b[0m---------'),
([12, 0], '+', '\x1b[34mTEST\x1b[0m--------+'),
([0, 0, 12], '+', '\x1b[34mTEST\x1b[0m----------'),
([12, 0, 0], '+', '\x1b[34mTEST\x1b[0m--------++'),
([3, 3], '+', '\x1b[34mTEST\x1b[0m---'),
([4, 2], '+', '\x1b[34mTEST\x1b[0m+--'),
([5, 1], '+', '\x1b[34mTEST\x1b[0m-+-'),
([3, 3, 3, 3], '+', '\x1b[34mTEST\x1b[0m---+---+---'),
([2, 2, 4, 4], '+', '\x1b[34mTEST\x1b[0m-+----+----'),
([1, 1, 5, 5], '+', '\x1b[34mTEST\x1b[0m-----+-----'),
([2, 2, 2, 2], '+', '\x1b[34mTEST\x1b[0m-+--+--'),
([1, 1, 1, 1, 1], '+', '\x1b[34mTEST\x1b[0m-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', '\x1b[34mTEST\x1b[0m++'),
([1, 1], '+', '\x1b[34mTES\x1b[0m'),
([1, 0], '+', '\x1b[34mTE\x1b[0m'),
([0, 0], '+', '\x1b[34mT\x1b[0m'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
@pytest.mark.parametrize('title', [
'\x1b[34mTEST\x1b[0m',
Color('{blue}TEST{/all}'),
Fore.BLUE + 'TEST' + Style.RESET_ALL,
colored('TEST', 'blue'),
])
def test_colors(column_widths, left, intersect, right, title, expected):
"""Test with color title characters.
:param iter column_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param title: Title in border with color codes.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(column_widths, '-', left, intersect, right, title=title)
assert ''.join(actual) == expected