# -*- coding: utf-8 -*-
# Copyright (c) 2009-2014, Luke Maurits <luke@maurits.id.au>
# All rights reserved.
# With contributions from:
# * Chris Clark
# * Klein Stephane
# * John Filleau
# PTable is forked from original Google Code page in April, 2015, and now
# maintained by Kane Blueriver <kxxoling@gmail.com>.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Taken from :
# https://github.com/kxxoling/PTable
# https://pypi.python.org/pypi/PTable
"""
The pretty table core
*********************
"""
import copy
import math
import random
import re
import sys
import textwrap
import unicodedata
from collections.abc import Iterable
from ._compact import basestring_
from ._compact import escape
from ._compact import itermap
from ._compact import str_types
from ._compact import uni_chr
from ._compact import unicode_
from typing import Optional, Sequence
PY2 = sys.version_info.major == 2
# hrule styles
FRAME = 0
ALL = 1
NONE = 2
HEADER = 3
# Table styles
DEFAULT = 10
MSWORD_FRIENDLY = 11
PLAIN_COLUMNS = 12
RANDOM = 20
_re = re.compile("\033\[[0-9;]*m")
def _get_size(text: str):
"""
:param text:
"""
lines = text.split("\n")
height = len(lines)
width = max([_str_block_width(line) for line in lines])
return width, height
[docs]
class PrettyTable(object):
def __init__(self, field_names: Optional[Sequence[str]] = None, **kwargs) -> None:
"""
:param encoding: Unicode encoding scheme used to decode any encoded input
:param title: optional table title
:param field_names: list or tuple of field names
:param fields: list or tuple of field names to include in displays
:param start: index of first data row to include in output
:param end: index of last data row to include in output PLUS ONE
:param header: print a header showing field names
:param header_style: stylisation to apply to field names in header
:param border: print a border around the table
:param hrules: controls printing of horizontal rules after rows
:param vrules: controls printing of vertical rules between columns
:param int_format: controls formatting of integer data
:param float_format: controls formatting of floating point data
:param min_table_width: minimum desired table width
:param max_table_width: maximum desired table width
:param padding_width: number of spaces on either side of column data
:param left_padding_width: number of spaces on left hand side of column data
:param right_padding_width: number of spaces on right hand side of column data
:param vertical_char: single character string used to draw vertical lines
:param horizontal_char: single character string used to draw horizontal lines
:param junction_char: single character string used to draw line junctions
:param sortby: name of field to sort rows by
:param sort_key: sorting key function
:param valign: default valign for each row
:param reversesort: True or False to sort in descending or ascending order
:param oldsortslice: Slice rows before sorting in the
"""
self.encoding = kwargs.get("encoding", "UTF-8")
# Data
self._field_names = []
self._rows = []
self.align = {}
self.valign = {}
self.max_width = {}
self.min_width = {}
self.int_format = {}
self.float_format = {}
if field_names:
self.field_names = field_names
else:
self._widths = []
# Options
self._options = "title start end fields header border sortby reversesort sort_key attributes format hrules vrules".split()
self._options.extend(
"int_format float_format min_table_width max_table_width padding_width left_padding_width right_padding_width".split()
)
self._options.extend(
"vertical_char horizontal_char junction_char header_style valign xhtml print_empty oldsortslice".split()
)
self._options.extend("align valign max_width min_width".split())
for option in self._options:
if option in kwargs:
self._validate_option(option, kwargs[option])
else:
kwargs[option] = None
self._title = kwargs["title"] or None
self._start = kwargs["start"] or 0
self._end = kwargs["end"] or None
self._fields = kwargs["fields"] or None
if kwargs["header"] in (True, False):
self._header = kwargs["header"]
else:
self._header = True
self._header_style = kwargs["header_style"] or None
if kwargs["border"] in (True, False):
self._border = kwargs["border"]
else:
self._border = True
self._hrules = kwargs["hrules"] or FRAME
self._vrules = kwargs["vrules"] or ALL
self._sortby = kwargs["sortby"] or None
if kwargs["reversesort"] in (True, False):
self._reversesort = kwargs["reversesort"]
else:
self._reversesort = False
self._sort_key = kwargs["sort_key"] or (lambda x: x)
# Column specific arguments, use property.setters
self.align = kwargs["align"] or {}
self.valign = kwargs["valign"] or {}
self.max_width = kwargs["max_width"] or {}
self.min_width = kwargs["min_width"] or {}
self.int_format = kwargs["int_format"] or {}
self.float_format = kwargs["float_format"] or {}
self._min_table_width = kwargs["min_table_width"] or None
self._max_table_width = kwargs["max_table_width"] or None
if kwargs["padding_width"] is None:
self._padding_width = 1
else:
self._padding_width = kwargs["padding_width"]
self._left_padding_width = kwargs["left_padding_width"] or None
self._right_padding_width = kwargs["right_padding_width"] or None
self._vertical_char = kwargs["vertical_char"] or self._unicode("|")
self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-")
self._junction_char = kwargs["junction_char"] or self._unicode("+")
if kwargs["print_empty"] in (True, False):
self._print_empty = kwargs["print_empty"]
else:
self._print_empty = True
if kwargs["oldsortslice"] in (True, False):
self._oldsortslice = kwargs["oldsortslice"]
else:
self._oldsortslice = False
self._format = kwargs["format"] or False
self._xhtml = kwargs["xhtml"] or False
self._attributes = kwargs["attributes"] or {}
def _unicode(self, value):
"""
:param value:
"""
if not isinstance(value, basestring_):
value = str(value)
if not isinstance(value, unicode_):
value = unicode_(value, self.encoding, "strict")
return value
def _justify(self, text: str, width: int, align):
"""
:param text: param width:
:param align:
:param width:
"""
excess = width - _str_block_width(text)
if align == "l":
return text + excess * " "
elif align == "r":
return excess * " " + text
else:
if excess % 2:
# Uneven padding
# Put more space on right if text is of odd length...
if _str_block_width(text) % 2:
return (excess // 2) * " " + text + (excess // 2 + 1) * " "
# and more space on left if text is of even length
else:
return (excess // 2 + 1) * " " + text + (excess // 2) * " "
# Why distribute extra space this way? To match the behaviour of
# the inbuilt str.center() method.
else:
# Equal padding on either side
return (excess // 2) * " " + text + (excess // 2) * " "
def __getattr__(self, name: str):
if name == "rowcount":
return len(self._rows)
elif name == "colcount":
if self._field_names:
return len(self._field_names)
elif self._rows:
return len(self._rows[0])
else:
return 0
else:
raise AttributeError(name)
def __getitem__(self, index):
new = PrettyTable()
new.field_names = self.field_names
for attr in self._options:
setattr(new, "_" + attr, getattr(self, "_" + attr))
setattr(new, "_align", getattr(self, "_align"))
if isinstance(index, slice):
for row in self._rows[index]:
new.add_row(row)
elif isinstance(index, int):
new.add_row(self._rows[index])
else:
raise Exception(
"Index %s is invalid, must be an integer or slice" % str(index)
)
return new
def __str__(self) -> str:
return self.__unicode__()
def __unicode__(self):
return self.get_string()
##############################
# ATTRIBUTE VALIDATORS #
##############################
# The method _validate_option is all that should be used elsewhere in the code base to validate options.
# It will call the appropriate validation method for that option. The individual validation methods should
# never need to be called directly (although nothing bad will happen if they *are*).
# Validation happens in TWO places.
# Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section.
# Secondly, in the _get_options method, where keyword arguments are mixed
# with persistent settings
def _validate_option(self, option, val) -> None:
"""
:param option: param val:
:param val:
"""
if option in "field_names":
self._validate_field_names(val)
elif option in (
"start",
"end",
"max_width",
"min_width",
"min_table_width",
"max_table_width",
"padding_width",
"left_padding_width",
"right_padding_width",
"format",
):
self._validate_nonnegative_int(option, val)
elif option in "sortby":
self._validate_field_name(option, val)
elif option in "sort_key":
self._validate_function(option, val)
elif option in "hrules":
self._validate_hrules(option, val)
elif option in "vrules":
self._validate_vrules(option, val)
elif option in "fields":
self._validate_all_field_names(option, val)
elif option in (
"header",
"border",
"reversesort",
"xhtml",
"print_empty",
"oldsortslice",
):
self._validate_true_or_false(option, val)
elif option in "header_style":
self._validate_header_style(val)
elif option in "int_format":
self._validate_int_format(option, val)
elif option in "float_format":
self._validate_float_format(option, val)
elif option in ("vertical_char", "horizontal_char", "junction_char"):
self._validate_single_char(option, val)
elif option in "attributes":
self._validate_attributes(option, val)
def _validate_field_names(self, val):
"""
:param val:
"""
# Check for appropriate length
if self._field_names:
try:
assert len(val) == len(self._field_names)
except AssertionError:
raise Exception(
"Field name list has incorrect number of values, (actual) %d!=%d (expected)"
% (len(val), len(self._field_names))
)
if self._rows:
try:
assert len(val) == len(self._rows[0])
except AssertionError:
raise Exception(
"Field name list has incorrect number of values, (actual) %d!=%d (expected)"
% (len(val), len(self._rows[0]))
)
# Check for uniqueness
try:
assert len(val) == len(set(val))
except AssertionError:
raise Exception("Field names must be unique!")
def _validate_header_style(self, val):
"""
:param val:
"""
try:
assert val in ("cap", "title", "upper", "lower", None)
except AssertionError:
raise Exception(
"Invalid header style, use cap, title, upper, lower or None!"
)
def _validate_align(self, val):
"""
:param val:
"""
try:
assert val in ["l", "c", "r"]
except AssertionError:
raise Exception("Alignment %s is invalid, use l, c or r!" % val)
def _validate_valign(self, val):
"""
:param val:
"""
try:
assert val in ["t", "m", "b", None]
except AssertionError:
raise Exception("Alignment %s is invalid, use t, m, b or None!" % val)
def _validate_nonnegative_int(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert int(val) >= 0
except AssertionError:
raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val)))
def _validate_true_or_false(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert val in (True, False)
except AssertionError:
raise Exception("Invalid value for %s! Must be True or False." % name)
def _validate_int_format(self, name: str, val):
"""
:param name: param val:
:param val:
"""
if val == "":
return
try:
assert type(val) in str_types
assert val.isdigit()
except AssertionError:
raise Exception(
"Invalid value for %s! Must be an integer format string." % name
)
def _validate_float_format(self, name: str, val):
"""
:param name: param val:
:param val:
"""
return
if val == "":
return
try:
val = val.rsplit("f")[0]
assert type(val) in str_types
assert "." in val
bits = val.split(".")
assert len(bits) <= 2
assert bits[0] == "" or bits[0].isdigit()
assert bits[1] == "" or bits[1].isdigit()
except AssertionError:
raise Exception(
"Invalid value for %s! Must be a float format string." % name
)
def _validate_function(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert hasattr(val, "__call__")
except AssertionError:
raise Exception("Invalid value for %s! Must be a function." % name)
def _validate_hrules(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert val in (ALL, FRAME, HEADER, NONE)
except AssertionError:
raise Exception(
"Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name
)
def _validate_vrules(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert val in (ALL, FRAME, NONE)
except AssertionError:
raise Exception(
"Invalid value for %s! Must be ALL, FRAME, or NONE." % name
)
def _validate_field_name(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert (val in self._field_names) or (val is None)
except AssertionError:
raise Exception("Invalid field name: %s!" % val)
def _validate_all_field_names(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
for x in val:
self._validate_field_name(name, x)
except AssertionError:
raise Exception("fields must be a sequence of field names!")
def _validate_single_char(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert _str_block_width(val) == 1
except AssertionError:
raise Exception(
"Invalid value for %s! Must be a string of length 1." % name
)
def _validate_attributes(self, name: str, val):
"""
:param name: param val:
:param val:
"""
try:
assert isinstance(val, dict)
except AssertionError:
raise Exception("attributes must be a dictionary of name/value pairs!")
##############################
# ATTRIBUTE MANAGEMENT #
##############################
@property
def field_names(self):
"""List or tuple of field names"""
return self._field_names
@field_names.setter
def field_names(self, val) -> None:
"""
:param val:
"""
val = [self._unicode(x) for x in val]
self._validate_option("field_names", val)
if self._field_names:
old_names = self._field_names[:]
self._field_names = val
if self._align and old_names:
for old_name, new_name in zip(old_names, val):
self._align[new_name] = self._align[old_name]
for old_name in old_names:
if old_name not in self._align:
self._align.pop(old_name)
else:
self.align = "c"
if self._valign and old_names:
for old_name, new_name in zip(old_names, val):
self._valign[new_name] = self._valign[old_name]
for old_name in old_names:
if old_name not in self._valign:
self._valign.pop(old_name)
else:
self.valign = "t"
@property
def align(self):
"""Controls alignment of fields
:param align: alignment
"""
return self._align
@align.setter
def align(self, val) -> None:
"""
:param val:
"""
if not self._field_names:
self._align = {}
elif val is None or (isinstance(val, dict) and len(val) == 0):
for field in self._field_names:
self._align[field] = "c"
else:
self._validate_align(val)
for field in self._field_names:
self._align[field] = val
@property
def valign(self):
"""Controls vertical alignment of fields
:param valign: vertical alignment
"""
return self._valign
@valign.setter
def valign(self, val) -> None:
"""
:param val:
"""
if not self._field_names:
self._valign = {}
elif val is None or (isinstance(val, dict) and len(val) == 0):
for field in self._field_names:
self._valign[field] = "t"
else:
self._validate_valign(val)
for field in self._field_names:
self._valign[field] = val
@property
def max_width(self):
"""Controls maximum width of fields
:param max_width: maximum width integer
"""
return self._max_width
@max_width.setter
def max_width(self, val) -> None:
"""
:param val:
"""
if val is None or (isinstance(val, dict) and len(val) == 0):
self._max_width = {}
else:
self._validate_option("max_width", val)
for field in self._field_names:
self._max_width[field] = val
@property
def min_width(self):
"""Controls minimum width of fields
:param min_width: minimum width integer
"""
if self.header:
fields = self._field_names
else:
fields = self._rows[0] if self._rows else []
result = {
# minimum column width can't be lesser
# than header's length
name: max(_str_block_width(unicode_(name)), self._min_width.get(name, 0))
for name in fields
}
return result
@min_width.setter
def min_width(self, val) -> None:
"""
:param val:
"""
if val is None or (isinstance(val, dict) and len(val) == 0):
self._min_width = {}
else:
self._validate_option("min_width", val)
for field in self._field_names:
self._min_width[field] = val
@property
def min_table_width(self):
""" """
return self._min_table_width
@min_table_width.setter
def min_table_width(self, val) -> None:
"""
:param val:
"""
self._validate_option("min_table_width", val)
self._min_table_width = val
@property
def max_table_width(self):
""" """
return self._max_table_width
@max_table_width.setter
def max_table_width(self, val) -> None:
"""
:param val:
"""
self._validate_option("max_table_width", val)
self._max_table_width = val
@property
def fields(self):
"""List or tuple of field names to include in displays"""
return self._fields
@fields.setter
def fields(self, val) -> None:
"""
:param val:
"""
self._validate_option("fields", val)
self._fields = val
@property
def title(self):
"""Optional table title
:param title: table title
"""
return self._title
@title.setter
def title(self, val) -> None:
"""
:param val:
"""
self._title = self._unicode(val)
@property
def start(self):
"""Start index of the range of rows to print
:param start: index of first data row to include in output
"""
return self._start
@start.setter
def start(self, val) -> None:
"""
:param val:
"""
self._validate_option("start", val)
self._start = val
@property
def end(self):
"""End index of the range of rows to print
:param end: index of last data row to include in output PLUS ONE
"""
return self._end
@end.setter
def end(self, val) -> None:
"""
:param val:
"""
self._validate_option("end", val)
self._end = val
@property
def sortby(self):
"""Name of field by which to sort rows
:param sortby: field name to sort by
"""
return self._sortby
@sortby.setter
def sortby(self, val) -> None:
"""
:param val:
"""
self._validate_option("sortby", val)
self._sortby = val
@property
def reversesort(self):
"""Controls direction of sorting (ascending vs descending)
:param reveresort: set to True to sort by descending order
"""
return self._reversesort
@reversesort.setter
def reversesort(self, val) -> None:
"""
:param val:
"""
self._validate_option("reversesort", val)
self._reversesort = val
@property
def sort_key(self):
"""Sorting key function, applied to data points before sorting
:param sort_key: a function which takes one argument and returns something to be sorted
"""
return self._sort_key
@sort_key.setter
def sort_key(self, val) -> None:
"""
:param val:
"""
self._validate_option("sort_key", val)
self._sort_key = val
@property
def header(self):
"""Controls printing of table header with field names
:param header: print a header showing field names
"""
return self._header
@header.setter
def header(self, val) -> None:
"""
:param val:
"""
self._validate_option("header", val)
self._header = val
@property
def header_style(self):
"""Controls stylisation applied to field names in header
:param header_style: stylisation to apply to field names in header
"""
return self._header_style
@header_style.setter
def header_style(self, val) -> None:
"""
:param val:
"""
self._validate_header_style(val)
self._header_style = val
@property
def border(self):
"""Controls printing of border around table
:param border: print a border around the table
"""
return self._border
@border.setter
def border(self, val) -> None:
"""
:param val:
"""
self._validate_option("border", val)
self._border = val
@property
def hrules(self):
"""Controls printing of horizontal rules after rows
:param hrules: horizontal rules style
"""
return self._hrules
@hrules.setter
def hrules(self, val) -> None:
"""
:param val:
"""
self._validate_option("hrules", val)
self._hrules = val
@property
def vrules(self):
"""Controls printing of vertical rules between columns
:param vrules: vertical rules style
"""
return self._vrules
@vrules.setter
def vrules(self, val) -> None:
"""
:param val:
"""
self._validate_option("vrules", val)
self._vrules = val
@property
def int_format(self):
"""Controls formatting of integer data
:param int_format: integer format string
"""
return self._int_format
@int_format.setter
def int_format(self, val) -> None:
"""
:param val:
"""
if val is None or (isinstance(val, dict) and len(val) == 0):
self._int_format = {}
else:
self._validate_option("int_format", val)
for field in self._field_names:
self._int_format[field] = val
@property
def float_format(self):
"""Controls formatting of floating point data
:param float_format: floating point format string
"""
return self._float_format
@float_format.setter
def float_format(self, val) -> None:
"""
:param val:
"""
if val is None or (isinstance(val, dict) and len(val) == 0):
self._float_format = {}
else:
self._validate_option("float_format", val)
for field in self._field_names:
self._float_format[field] = val
@property
def padding_width(self):
"""The number of empty spaces between a column's edge and its content
:param padding_width: number of spaces
"""
return self._padding_width
@padding_width.setter
def padding_width(self, val) -> None:
"""
:param val:
"""
self._validate_option("padding_width", val)
self._padding_width = val
@property
def left_padding_width(self):
"""The number of empty spaces between a column's left edge and its content
:param left_padding: number of spaces
"""
return self._left_padding_width
@left_padding_width.setter
def left_padding_width(self, val) -> None:
"""
:param val:
"""
self._validate_option("left_padding_width", val)
self._left_padding_width = val
@property
def right_padding_width(self):
"""The number of empty spaces between a column's right edge and its content
:param right_padding: number of spaces
"""
return self._right_padding_width
@right_padding_width.setter
def right_padding_width(self, val) -> None:
"""
:param val:
"""
self._validate_option("right_padding_width", val)
self._right_padding_width = val
@property
def vertical_char(self):
"""The charcter used when printing table borders to draw vertical lines
:param vertical_char: single character string used to draw vertical lines
"""
return self._vertical_char
@vertical_char.setter
def vertical_char(self, val) -> None:
"""
:param val:
"""
val = self._unicode(val)
self._validate_option("vertical_char", val)
self._vertical_char = val
@property
def horizontal_char(self):
"""The charcter used when printing table borders to draw horizontal lines
:param horizontal_char: single character string used to draw horizontal lines
"""
return self._horizontal_char
@horizontal_char.setter
def horizontal_char(self, val) -> None:
"""
:param val:
"""
val = self._unicode(val)
self._validate_option("horizontal_char", val)
self._horizontal_char = val
@property
def junction_char(self):
"""The charcter used when printing table borders to draw line junctions
:param junction_char: single character string used to draw line junctions
"""
return self._junction_char
@junction_char.setter
def junction_char(self, val) -> None:
"""
:param val:
"""
val = self._unicode(val)
self._validate_option("vertical_char", val)
self._junction_char = val
@property
def format(self):
"""Controls whether or not HTML tables are formatted to match styling options
:param format: True or False
"""
return self._format
@format.setter
def format(self, val) -> None:
"""
:param val:
"""
self._validate_option("format", val)
self._format = val
@property
def print_empty(self):
"""Controls whether or not empty tables produce a header and frame or just an empty string
:param print_empty: True or False
"""
return self._print_empty
@print_empty.setter
def print_empty(self, val) -> None:
"""
:param val:
"""
self._validate_option("print_empty", val)
self._print_empty = val
@property
def attributes(self):
"""A dictionary of HTML attribute name/value pairs to be included in the <table> tag when printing HTML
Arguments:
attributes - dictionary of attributes
"""
return self._attributes
@attributes.setter
def attributes(self, val) -> None:
"""
:param val:
"""
self._validate_option("attributes", val)
self._attributes = val
@property
def oldsortslice(self):
"""oldsortslice - Slice rows before sorting in the 'old style'"""
return self._oldsortslice
@oldsortslice.setter
def oldsortslice(self, val) -> None:
"""
:param val:
"""
self._validate_option("oldsortslice", val)
self._oldsortslice = val
##############################
# OPTION MIXER #
##############################
def _get_options(self, kwargs):
"""
:param kwargs:
"""
options = {}
for option in self._options:
if option in kwargs:
self._validate_option(option, kwargs[option])
options[option] = kwargs[option]
else:
options[option] = getattr(self, "_" + option)
return options
##############################
# PRESET STYLE LOGIC #
##############################
[docs]
def set_style(self, style):
"""
:param style:
"""
if style == DEFAULT:
self._set_default_style()
elif style == MSWORD_FRIENDLY:
self._set_msword_style()
elif style == PLAIN_COLUMNS:
self._set_columns_style()
elif style == RANDOM:
self._set_random_style()
else:
raise Exception("Invalid pre-set style!")
def _set_default_style(self) -> None:
""" """
self.header = True
self.border = True
self._hrules = FRAME
self._vrules = ALL
self.padding_width = 1
self.left_padding_width = 1
self.right_padding_width = 1
self.vertical_char = "|"
self.horizontal_char = "-"
self.junction_char = "+"
def _set_msword_style(self) -> None:
""" """
self.header = True
self.border = True
self._hrules = NONE
self.padding_width = 1
self.left_padding_width = 1
self.right_padding_width = 1
self.vertical_char = "|"
def _set_columns_style(self) -> None:
""" """
self.header = True
self.border = False
self.padding_width = 1
self.left_padding_width = 0
self.right_padding_width = 8
def _set_random_style(self) -> None:
""" """
# Just for fun!
self.header = random.choice((True, False))
self.border = random.choice((True, False))
self._hrules = random.choice((ALL, FRAME, HEADER, NONE))
self._vrules = random.choice((ALL, FRAME, NONE))
self.left_padding_width = random.randint(0, 5)
self.right_padding_width = random.randint(0, 5)
self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
##############################
# DATA INPUT METHODS #
##############################
[docs]
def add_row(self, row: Iterable[str]) -> None:
"""Add a row to the table
:param row: row of data
:param has: fields
"""
if self._field_names and len(row) != len(self._field_names):
raise Exception(
"Row has incorrect number of values, (actual) %d!=%d (expected)"
% (len(row), len(self._field_names))
)
if not self._field_names:
self.field_names = [("Field %d" % (n + 1)) for n in range(0, len(row))]
self._rows.append(list(row))
[docs]
def del_row(self, row_index):
"""Delete a row to the table
:param row_index: The index of the row you want to delete
"""
if row_index > len(self._rows) - 1:
raise Exception(
"Cant delete row at index %d, table only has %d rows!"
% (row_index, len(self._rows))
)
del self._rows[row_index]
[docs]
def add_column(self, fieldname: str, column, align: str = "c", valign: str = "t"):
"""Add a column to the table.
:param fieldname: name of the field to contain the new column of data
:param column: column of data
:param table: has rows
:param align: desired alignment for this column (Default value = "c")
:param valign: desired vertical alignment for new columns (Default value = "t")
"""
if len(self._rows) in (0, len(column)):
self._validate_align(align)
self._validate_valign(valign)
self._field_names.append(fieldname)
self._align[fieldname] = align
self._valign[fieldname] = valign
for i in range(0, len(column)):
if len(self._rows) < i + 1:
self._rows.append([])
self._rows[i].append(column[i])
else:
raise Exception(
"Column length %d does not match number of rows %d!"
% (len(column), len(self._rows))
)
[docs]
def clear_rows(self) -> None:
"""Delete all rows from the table but keep the current field names"""
self._rows = []
[docs]
def clear(self) -> None:
"""Delete all rows and field names from the table, maintaining nothing but styling options"""
self._rows = []
self._field_names = []
self._widths = []
##############################
# MISC PUBLIC METHODS #
##############################
[docs]
def copy(self):
""" """
return copy.deepcopy(self)
##############################
# MISC PRIVATE METHODS #
##############################
def _format_value(self, field, value):
"""
:param field: param value:
:param value:
"""
if isinstance(value, int) and field in self._int_format:
value = self._unicode(("%%%sd" % self._int_format[field]) % value)
elif isinstance(value, float) and field in self._float_format:
value = self._unicode((self._float_format[field]) % value)
return self._unicode(value)
def _compute_table_width(self, options):
"""
:param options:
"""
table_width = 2 if options["vrules"] in (FRAME, ALL) else 0
per_col_padding = sum(self._get_padding_widths(options))
for index, fieldname in enumerate(self.field_names):
if not options["fields"] or (
options["fields"] and fieldname in options["fields"]
):
table_width += self._widths[index] + per_col_padding
return table_width
def _compute_widths(self, rows, options) -> None:
"""
:param rows: param options:
:param options:
"""
if options["header"]:
widths = [_get_size(field)[0] for field in self._field_names]
else:
widths = len(self.field_names) * [0]
for row in rows:
for index, value in enumerate(row):
fieldname = self.field_names[index]
if fieldname in self.max_width:
widths[index] = max(
widths[index],
min(_get_size(value)[0], self.max_width[fieldname]),
)
else:
widths[index] = max(widths[index], _get_size(value)[0])
if fieldname in self.min_width:
widths[index] = max(widths[index], self.min_width[fieldname])
self._widths = widths
# Are we exceeding max_table_width?
if self._max_table_width:
table_width = self._compute_table_width(options)
if table_width > self._max_table_width:
# get dict with minimum widths for fields
min_width = self.min_width
# first calculate width for paddings and vrules this
# space we can't shrink.
# Space for vrules
nonshrinkable = 2 if options["vrules"] in (FRAME, ALL) else 0
# Space for vrules between columns
nonshrinkable += len(self._field_names) - 1
# Space for padding in each column
per_col_padding = sum(self._get_padding_widths(options))
nonshrinkable += len(widths) * per_col_padding
# Min space for each column
nonshrinkable += sum(min_width.values())
# Shrink widths in proportion
scale = float(self._max_table_width - nonshrinkable) / (
table_width - nonshrinkable
)
def calculate_new_width(field_name: str, old_width):
"""
:param field_name: param old_width:
:param old_width:
"""
width = min_width[field_name]
# scale according to recalculated table width
scaled_part = int(math.floor((old_width - width) * scale))
# enforce minimum column width as 1 symbol
return max(1, width + scaled_part)
widths = list(map(calculate_new_width, self._field_names, widths))
self._widths = widths
# Are we under min_table_width or title width?
if self._min_table_width or options["title"]:
if options["title"]:
title_width = len(options["title"]) + sum(
self._get_padding_widths(options)
)
if options["vrules"] in (FRAME, ALL):
title_width += 2
else:
title_width = 0
min_table_width = self.min_table_width or 0
min_width = max(title_width, min_table_width)
table_width = self._compute_table_width(options)
if table_width < min_width:
# Grow widths in proportion
scale = 1.0 * min_width / table_width
widths = [int(math.ceil(w * scale)) for w in widths]
self._widths = widths
def _get_padding_widths(self, options):
"""
:param options:
"""
if options["left_padding_width"] is not None:
lpad = options["left_padding_width"]
else:
lpad = options["padding_width"]
if options["right_padding_width"] is not None:
rpad = options["right_padding_width"]
else:
rpad = options["padding_width"]
return lpad, rpad
def _get_rows(self, options):
"""
:param options: dictionary of option settings
"""
if options["oldsortslice"]:
rows = copy.deepcopy(self._rows[options["start"] : options["end"]])
else:
rows = copy.deepcopy(self._rows)
# Sort
if options["sortby"]:
sortindex = self._field_names.index(options["sortby"])
# Decorate
rows = [[row[sortindex]] + row for row in rows]
# Sort
rows.sort(reverse=options["reversesort"], key=options["sort_key"])
# Undecorate
rows = [row[1:] for row in rows]
# Slice if necessary
if not options["oldsortslice"]:
rows = rows[options["start"] : options["end"]]
return rows
def _format_row(self, row, options):
"""
:param row: param options:
:param options:
"""
return [
self._format_value(field, value)
for (field, value) in zip(self._field_names, row)
]
def _format_rows(self, rows, options):
"""
:param rows: param options:
:param options:
"""
return [self._format_row(row, options) for row in rows]
##############################
# PLAIN TEXT STRING METHODS #
##############################
[docs]
def get_string(self, **kwargs) -> str:
"""
:param title: optional table title
:param start: index of first data row to include in output
:param end: index of last data row to include in output PLUS ONE
:param fields: names of fields
:param header: print a header showing field names
:param border: print a border around the table
:param hrules: controls printing of horizontal rules after rows
:param vrules: controls printing of vertical rules between columns
:param int_format: controls formatting of integer data
:param float_format: controls formatting of floating point data
:param padding_width: number of spaces on either side of column data
:param left_padding_width: number of spaces on left hand side of column data
:param right_padding_width: number of spaces on right hand side of column data
:param vertical_char: single character string used to draw vertical lines
:param horizontal_char: single character string used to draw horizontal lines
:param junction_char: single character string used to draw line junctions
:param sortby: name of field to sort rows by
:param sort_key: sorting key function
:param reversesort: True or False to sort in descending or ascending order
:param print: empty
"""
options = self._get_options(kwargs)
lines = []
# Don't think too hard about an empty table
# Is this the desired behaviour? Maybe we should still print the
# header?
if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
return ""
# Get the rows we need to print, taking into account slicing, sorting,
# etc.
rows = self._get_rows(options)
# Turn all data in all rows into Unicode, formatted as desired
formatted_rows = self._format_rows(rows, options)
# Compute column widths
self._compute_widths(formatted_rows, options)
self._hrule = self._stringify_hrule(options)
# Add title
title = options["title"] or self._title
if title:
lines.append(self._stringify_title(title, options))
# Add header or top of border
if options["header"]:
lines.append(self._stringify_header(options))
elif options["border"] and options["hrules"] in (ALL, FRAME):
lines.append(self._hrule)
# Add rows
for row in formatted_rows:
lines.append(self._stringify_row(row, options))
# Add bottom of border
if options["border"] and options["hrules"] == FRAME:
lines.append(self._hrule)
return self._unicode("\n").join(lines)
def _stringify_hrule(self, options):
"""
:param options:
"""
if not options["border"]:
return ""
lpad, rpad = self._get_padding_widths(options)
if options["vrules"] in (ALL, FRAME):
bits = [options["junction_char"]]
else:
bits = [options["horizontal_char"]]
# For tables with no data or fieldnames
if not self._field_names:
bits.append(options["junction_char"])
return "".join(bits)
for field, width in zip(self._field_names, self._widths):
if options["fields"] and field not in options["fields"]:
continue
bits.append((width + lpad + rpad) * options["horizontal_char"])
if options["vrules"] == ALL:
bits.append(options["junction_char"])
else:
bits.append(options["horizontal_char"])
if options["vrules"] == FRAME:
bits.pop()
bits.append(options["junction_char"])
return "".join(bits)
def _stringify_title(self, title, options):
"""
:param title: param options:
:param options:
"""
lines = []
lpad, rpad = self._get_padding_widths(options)
if options["border"]:
if options["vrules"] == ALL:
options["vrules"] = FRAME
lines.append(self._stringify_hrule(options))
options["vrules"] = ALL
elif options["vrules"] == FRAME:
lines.append(self._stringify_hrule(options))
bits = []
endpoint = (
options["vertical_char"] if options["vrules"] in (ALL, FRAME) else " "
)
bits.append(endpoint)
title = " " * lpad + title + " " * rpad
bits.append(self._justify(title, len(self._hrule) - 2, "c"))
bits.append(endpoint)
lines.append("".join(bits))
return "\n".join(lines)
def _stringify_header(self, options):
"""
:param options:
"""
bits = []
lpad, rpad = self._get_padding_widths(options)
if options["border"]:
if options["hrules"] in (ALL, FRAME):
bits.append(self._hrule)
bits.append("\n")
if options["vrules"] in (ALL, FRAME):
bits.append(options["vertical_char"])
else:
bits.append(" ")
# For tables with no data or field names
if not self._field_names:
if options["vrules"] in (ALL, FRAME):
bits.append(options["vertical_char"])
else:
bits.append(" ")
for (
field,
width,
) in zip(self._field_names, self._widths):
if options["fields"] and field not in options["fields"]:
continue
if self._header_style == "cap":
fieldname = field.capitalize()
elif self._header_style == "title":
fieldname = field.title()
elif self._header_style == "upper":
fieldname = field.upper()
elif self._header_style == "lower":
fieldname = field.lower()
else:
fieldname = field
bits.append(
" " * lpad
+ self._justify(fieldname, width, self._align[field])
+ " " * rpad
)
if options["border"]:
if options["vrules"] == ALL:
bits.append(options["vertical_char"])
else:
bits.append(" ")
# If vrules is FRAME, then we just appended a space at the end
# of the last field, when we really want a vertical character
if options["border"] and options["vrules"] == FRAME:
bits.pop()
bits.append(options["vertical_char"])
if options["border"] and options["hrules"] != NONE:
bits.append("\n")
bits.append(self._hrule)
return "".join(bits)
def _stringify_row(self, row, options):
"""
:param row: param options:
:param options:
"""
for (
index,
field,
value,
width,
) in zip(list(range(0, len(row))), self._field_names, row, self._widths):
# Enforce max widths
lines = value.split("\n")
new_lines = []
for line in lines:
if _str_block_width(line) > width:
line = textwrap.fill(line, width)
new_lines.append(line)
lines = new_lines
value = "\n".join(lines)
row[index] = value
row_height = 0
for c in row:
h = _get_size(c)[1]
if h > row_height:
row_height = h
bits = []
lpad, rpad = self._get_padding_widths(options)
for y in range(0, row_height):
bits.append([])
if options["border"]:
if options["vrules"] in (ALL, FRAME):
bits[y].append(self.vertical_char)
else:
bits[y].append(" ")
for (
field,
value,
width,
) in zip(self._field_names, row, self._widths):
valign = self._valign[field]
lines = value.split("\n")
dHeight = row_height - len(lines)
if dHeight:
if valign == "m":
lines = (
[""] * int(dHeight / 2)
+ lines
+ [""] * (dHeight - int(dHeight / 2))
)
elif valign == "b":
lines = [""] * dHeight + lines
else:
lines += [""] * dHeight
y = 0
for l in lines:
if options["fields"] and field not in options["fields"]:
continue
bits[y].append(
" " * lpad
+ self._justify(l, width, self._align[field])
+ " " * rpad
)
if options["border"]:
if options["vrules"] == ALL:
bits[y].append(self.vertical_char)
else:
bits[y].append(" ")
y += 1
# If vrules is FRAME, then we just appended a space at the end
# of the last field, when we really want a vertical character
for y in range(0, row_height):
if options["border"] and options["vrules"] == FRAME:
bits[y].pop()
bits[y].append(options["vertical_char"])
if options["border"] and options["hrules"] == ALL:
bits[row_height - 1].append("\n")
bits[row_height - 1].append(self._hrule)
for y in range(0, row_height):
bits[y] = "".join(bits[y])
return "\n".join(bits)
[docs]
def paginate(self, page_length: int = 58, **kwargs):
"""
:param page_length: Default value = 58)
"""
pages = []
kwargs["start"] = kwargs.get("start", 0)
true_end = kwargs.get("end", self.rowcount)
while True:
kwargs["end"] = min(kwargs["start"] + page_length, true_end)
pages.append(self.get_string(**kwargs))
if kwargs["end"] == true_end:
break
kwargs["start"] += page_length
return "\f".join(pages)
##############################
# HTML STRING METHODS #
##############################
[docs]
def get_html_string(self, **kwargs) -> str:
"""
:param title: optional table title
:param start: index of first data row to include in output
:param end: index of last data row to include in output PLUS ONE
:param fields: names of fields
:param header: print a header showing field names
:param border: print a border around the table
:param hrules: controls printing of horizontal rules after rows
:param vrules: controls printing of vertical rules between columns
:param int_format: controls formatting of integer data
:param float_format: controls formatting of floating point data
:param padding_width: number of spaces on either side of column data
:param left_padding_width: number of spaces on left hand side of column data
:param right_padding_width: number of spaces on right hand side of column data
:param sortby: name of field to sort rows by
:param sort_key: sorting key function
:param attributes: dictionary of name
:param xhtml: print
"""
options = self._get_options(kwargs)
if options["format"]:
string = self._get_formatted_html_string(options)
else:
string = self._get_simple_html_string(options)
return string
def _get_simple_html_string(self, options):
"""
:param options:
"""
lines = []
if options["xhtml"]:
linebreak = "<br/>"
else:
linebreak = "<br>"
open_tag = list()
open_tag.append("<table")
if options["attributes"]:
for attr_name in options["attributes"]:
open_tag.append(
' %s="%s"' % (attr_name, options["attributes"][attr_name])
)
open_tag.append(">")
lines.append("".join(open_tag))
# Title
title = options["title"] or self._title
if title:
cols = (
len(options["fields"]) if options["fields"] else len(self.field_names)
)
lines.append(" <tr>")
lines.append(" <td colspan=%d>%s</td>" % (cols, title))
lines.append(" </tr>")
# Headers
if options["header"]:
lines.append(" <tr>")
for field in self._field_names:
if options["fields"] and field not in options["fields"]:
continue
lines.append(
" <th style='text-align: left;'>%s</th>"
% escape(field).replace("\n", linebreak)
)
lines.append(" </tr>")
# Data
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows, options)
for row in formatted_rows:
lines.append(" <tr>")
for field, datum in zip(self._field_names, row):
if options["fields"] and field not in options["fields"]:
continue
lines.append(
" <td>%s</td>" % escape(datum).replace("\n", linebreak)
)
lines.append(" </tr>")
lines.append("</table>")
return self._unicode("\n").join(lines)
def _get_formatted_html_string(self, options):
"""
:param options:
"""
lines = []
lpad, rpad = self._get_padding_widths(options)
if options["xhtml"]:
linebreak = "<br/>"
else:
linebreak = "<br>"
open_tag = list()
open_tag.append("<table")
if options["border"]:
if options["hrules"] == ALL and options["vrules"] == ALL:
open_tag.append(' frame="box" rules="all"')
elif options["hrules"] == FRAME and options["vrules"] == FRAME:
open_tag.append(' frame="box"')
elif options["hrules"] == FRAME and options["vrules"] == ALL:
open_tag.append(' frame="box" rules="cols"')
elif options["hrules"] == FRAME:
open_tag.append(' frame="hsides"')
elif options["hrules"] == ALL:
open_tag.append(' frame="hsides" rules="rows"')
elif options["vrules"] == FRAME:
open_tag.append(' frame="vsides"')
elif options["vrules"] == ALL:
open_tag.append(' frame="vsides" rules="cols"')
if options["attributes"]:
for attr_name in options["attributes"]:
open_tag.append(
' %s="%s"' % (attr_name, options["attributes"][attr_name])
)
open_tag.append(">")
lines.append("".join(open_tag))
# Title
title = options["title"] or self._title
if title:
cols = (
len(options["fields"]) if options["fields"] else len(self.field_names)
)
lines.append(" <tr>")
lines.append(" <td colspan=%d>%s</td>" % (cols, title))
lines.append(" </tr>")
# Headers
if options["header"]:
lines.append(" <tr>")
for field in self._field_names:
if options["fields"] and field not in options["fields"]:
continue
lines.append(
' <th style="padding-left: %dem; padding-right: %dem; text-align: left;">%s</th>'
% (lpad, rpad, escape(field).replace("\n", linebreak))
)
lines.append(" </tr>")
# Data
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows, options)
aligns = []
valigns = []
for field in self._field_names:
aligns.append(
{"l": "left", "r": "right", "c": "center"}[self._align[field]]
)
valigns.append(
{"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
)
for row in formatted_rows:
lines.append(" <tr>")
for field, datum, align, valign in zip(
self._field_names, row, aligns, valigns
):
if options["fields"] and field not in options["fields"]:
continue
lines.append(
' <td style="padding-left: %dem; padding-right: %dem; text-align: %s; vertical-align: %s">%s</td>'
% (
lpad,
rpad,
align,
valign,
escape(datum).replace("\n", linebreak),
)
)
lines.append(" </tr>")
lines.append("</table>")
return self._unicode("\n").join(lines)
##############################
# UNICODE WIDTH FUNCTIONS #
##############################
def _char_block_width(char: str):
"""
:param char:
"""
# Basic Latin, which is probably the most common case
# if char in range(0x0021, 0x007e):
# if char >= 0x0021 and char <= 0x007e:
if 0x0021 <= char <= 0x007E:
return 1
# Chinese, Japanese, Korean (common)
if 0x4E00 <= char <= 0x9FFF:
return 2
# Hangul
if 0xAC00 <= char <= 0xD7AF:
return 2
# Combining?
if unicodedata.combining(uni_chr(char)):
return 0
# Hiragana and Katakana
if 0x3040 <= char <= 0x309F or 0x30A0 <= char <= 0x30FF:
return 2
# Full-width Latin characters
if 0xFF01 <= char <= 0xFF60:
return 2
# CJK punctuation
if 0x3000 <= char <= 0x303E:
return 2
# Backspace and delete
if char in (0x0008, 0x007F):
return -1
# Other control characters
elif char in (0x0000, 0x000F, 0x001F):
return 0
# Take a guess
return 1
def _str_block_width(val):
"""
:param val:
"""
return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val))))