"""This module provides BeautifulTable class
It is intended for printing Tabular data to terminals.
Example
-------
>>> from beautifultable import BeautifulTable
>>> table = BeautifulTable()
>>> table.columns.header = ['1st column', '2nd column']
>>> for i in range(5):
... table.rows.apppend([i, i*i])
...
>>> print(table)
+------------+------------+
| 1st column | 2nd column |
+------------+------------+
| 0 | 0 |
+------------+------------+
| 1 | 1 |
+------------+------------+
| 2 | 4 |
+------------+------------+
| 3 | 9 |
+------------+------------+
| 4 | 16 |
+------------+------------+
"""
from __future__ import division, unicode_literals
import copy
import csv
import warnings
from . import enums
from .utils import (
pre_process,
termwidth,
deprecated,
deprecated_param,
deprecation_message,
ensure_type,
)
from .compat import basestring, Iterable, to_unicode
from .base import BTBaseList
from .helpers import (
BTRowCollection,
BTColumnCollection,
BTRowHeader,
BTColumnHeader,
)
__all__ = [
"BeautifulTable",
"BTRowCollection",
"BTColumnCollection",
"BTRowHeader",
"BTColumnHeader",
"BTBorder",
]
[docs]class BTBorder:
"""Class to control how each section of the table's border is rendered.
To disable a behaviour, just set its corresponding attribute
to an empty string
Attributes
----------
top : str
Character used to draw the top border.
left : str
Character used to draw the left border.
bottom : str
Character used to draw the bottom border.
right : str
Character used to draw the right border.
top_left : str
Left most character of the top border.
bottom_left : str
Left most character of the bottom border.
bottom_right : str
Right most character of the bottom border.
top_right : str
Right most character of the top border.
header_left : str
Left most character of the header separator.
header_right : str
Right most character of the header separator.
top_junction : str
Junction character for top border.
left_junction : str
Junction character for left border.
bottom_junction : str
Junction character for bottom border.
right_junction : str
Junction character for right border.
"""
def __init__(
self,
top,
left,
bottom,
right,
top_left,
bottom_left,
bottom_right,
top_right,
header_left,
header_right,
top_junction,
left_junction,
bottom_junction,
right_junction,
):
self.top = top
self.left = left
self.bottom = bottom
self.right = right
self.top_left = top_left
self.bottom_left = bottom_left
self.bottom_right = bottom_right
self.top_right = top_right
self.header_left = header_left
self.header_right = header_right
self.top_junction = top_junction
self.left_junction = left_junction
self.bottom_junction = bottom_junction
self.right_junction = right_junction
def _make_getter(attr):
return lambda self: getattr(self, attr)
def _make_setter(attr):
return lambda self, value: setattr(self, attr, ensure_type(value, basestring))
for prop, attr in [
(x, "_{}".format(x))
for x in (
"top",
"left",
"bottom",
"right",
"top_left",
"bottom_left",
"bottom_right",
"top_right",
"header_left",
"header_right",
"top_junction",
"left_junction",
"bottom_junction",
"right_junction",
)
]:
setattr(BTBorder, prop, property(_make_getter(attr), _make_setter(attr)))
class BTTableData(BTBaseList):
def __init__(self, table, value=None):
if value is None:
value = []
self._table = table
self._value = value
def _get_canonical_key(self, key):
return self._table.rows._canonical_key(key)
def _get_ideal_length(self):
pass
[docs]class BeautifulTable:
"""Utility Class to print data in tabular format to terminal.
Parameters
----------
maxwidth: int, optional
maximum width of the table in number of characters. this is ignored
when manually setting the width of the columns. if this value is too
low with respect to the number of columns and width of padding, the
resulting table may override it(default 80).
default_alignment : int, optional
Default alignment for new columns(default beautifultable.ALIGN_CENTER).
default_padding : int, optional
Default width of the left and right padding for new columns(default 1).
precision : int, optional
All float values will have maximum number of digits after the decimal,
capped by this value(Default 3).
serialno : bool, optional
If true, a column will be rendered with serial numbers(**DEPRECATED**).
serialno_header: str, optional
The header of the serial number column if rendered(**DEPRECATED**).
detect_numerics : bool, optional
Whether numeric strings should be automatically detected(Default True).
sign : SignMode, optional
Parameter to control how signs in numeric data are displayed.
(default beautifultable.SM_MINUS).
Attributes
----------
precision : int
All float values will have maximum number of digits after the decimal,
capped by this value(Default 3).
detect_numerics : bool
Whether numeric strings should be automatically detected(Default True).
"""
@deprecated_param("1.0.0", "1.2.0", "sign_mode", "sign")
@deprecated_param("1.0.0", "1.2.0", "numeric_precision", "precision")
@deprecated_param("1.0.0", "1.2.0", "max_width", "maxwidth")
@deprecated_param("1.0.0", "1.2.0", "serialno")
@deprecated_param("1.0.0", "1.2.0", "serialno_header")
def __init__(
self,
maxwidth=80,
default_alignment=enums.ALIGN_CENTER,
default_padding=1,
precision=3,
serialno=False,
serialno_header="SN",
detect_numerics=True,
sign=enums.SM_MINUS,
**kwargs,
):
kwargs.setdefault("max_width", None)
if kwargs["max_width"] is not None:
maxwidth = kwargs["max_width"]
kwargs.setdefault("numeric_precision", None)
if kwargs["numeric_precision"] is not None:
precision = kwargs["numeric_precision"]
kwargs.setdefault("sign_mode", None)
if kwargs["sign_mode"] is not None:
sign = kwargs["sign_mode"]
self.precision = precision
self._serialno = serialno
self._serialno_header = serialno_header
self.detect_numerics = detect_numerics
self._sign = sign
self.maxwidth = maxwidth
self._ncol = 0
self._data = BTTableData(self)
self.rows = BTRowCollection(self)
self.columns = BTColumnCollection(self, default_alignment, default_padding)
self._header_separator = ""
self._header_junction = ""
self._column_separator = ""
self._row_separator = ""
self.border = ""
self.set_style(enums.STYLE_DEFAULT)
def __copy__(self):
obj = type(self)()
obj.__dict__.update({k: copy.copy(v) for k, v in self.__dict__.items()})
obj.rows._table = obj
obj.rows.header._table = obj
obj.columns._table = obj
obj.columns.header._table = obj
obj.columns.alignment._table = obj
obj.columns.width._table = obj
obj.columns.padding_left._table = obj
obj.columns.padding_right._table = obj
obj._data._table = obj
for row in obj._data:
row._table = obj
return obj
def __deepcopy__(self, memo):
obj = type(self)()
obj.__dict__.update(
{k: copy.deepcopy(v, memo) for k, v in self.__dict__.items()}
)
obj.rows._table = obj
obj.rows.header._table = obj
obj.columns._table = obj
obj.columns.header._table = obj
obj.columns.alignment._table = obj
obj.columns.width._table = obj
obj.columns.padding_left._table = obj
obj.columns.padding_right._table = obj
obj._data._table = obj
for row in obj._data:
row._table = obj
return obj
def __setattr__(self, name, value):
attrs = (
"left_border_char",
"right_border_char",
"top_border_char",
"bottom_border_char",
"header_separator_char",
"column_separator_char",
"row_separator_char",
"intersect_top_left",
"intersect_top_mid",
"intersect_top_right",
"intersect_header_left",
"intersect_header_mid",
"intersect_header_right",
"intersect_row_left",
"intersect_row_mid",
"intersect_row_right",
"intersect_bottom_left",
"intersect_bottom_mid",
"intersect_bottom_right",
)
if to_unicode(name) in attrs:
warnings.warn(
deprecation_message(name, "1.0.0", "1.2.0", None),
FutureWarning,
)
value = ensure_type(value, basestring, name)
super(BeautifulTable, self).__setattr__(name, value)
@deprecated(
"1.0.0",
"1.2.0",
BTRowCollection.__len__,
details="Use len(BeautifulTable.rows)' instead.",
)
def __len__(self): # pragma: no cover
return len(self.rows)
@deprecated(
"1.0.0" "1.2.0",
BTRowCollection.__iter__,
details="Use iter(BeautifulTable.rows)' instead.",
)
def __iter__(self): # pragma: no cover
return iter(self.rows)
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__contains__,
details="Use ''value' in BeautifulTable.{columns|rows}' instead.",
)
def __contains__(self, key): # pragma: no cover
if isinstance(key, basestring):
return key in self.columns
elif isinstance(key, Iterable):
return key in self.rows
else:
raise TypeError(f"'key' must be str or Iterable, not {type(key).__name__}")
def __repr__(self):
return repr(self._data)
def __str__(self):
if len(self.rows) == 0 or len(self.columns) == 0:
return ""
string_ = []
for line in self._get_string([], append=False):
string_.append(line)
return "\n".join(string_)
# ************************Properties Begin Here************************
@property
def shape(self):
"""Read only attribute which returns the shape of the table."""
return (len(self.rows), len(self.columns))
@property
def sign(self):
"""Attribute to control how signs are displayed for numerical data.
It can be one of the following:
======================== =============================================
Option Meaning
======================== =============================================
beautifultable.SM_PLUS A sign should be used for both +ve and -ve
numbers.
beautifultable.SM_MINUS A sign should only be used for -ve numbers.
beautifultable.SM_SPACE A leading space should be used for +ve
numbers and a minus sign for -ve numbers.
======================== =============================================
"""
return self._sign
@sign.setter
def sign(self, value):
if not isinstance(value, enums.SignMode):
allowed = (f"{type(self).__name__}.{i.name}" for i in enums.SignMode)
error_msg = "allowed values for sign are: " + ", ".join(allowed)
raise ValueError(error_msg)
self._sign = value
@property
def border(self):
"""Characters used to draw the border of the table.
You can set this directly to a character or use it's several attribute
to control how each section of the table is rendered.
It is an instance of :class:`~.BTBorder`
"""
return self._border
@border.setter
def border(self, value):
self._border = BTBorder(
top=value,
left=value,
bottom=value,
right=value,
top_left=value,
bottom_left=value,
bottom_right=value,
top_right=value,
header_left=value,
header_right=value,
top_junction=value,
left_junction=value,
bottom_junction=value,
right_junction=value,
)
@property
def junction(self):
"""Character used to draw junctions in the row separator."""
return self._junction
@junction.setter
def junction(self, value):
self._junction = ensure_type(value, basestring)
@property
@deprecated("1.0.0", "1.2.0", BTRowCollection.header.fget)
def serialno(self): # pragma: no cover
return self._serialno
@serialno.setter
@deprecated("1.0.0", "1.2.0", BTRowCollection.header.fget)
def serialno(self, value): # pragma: no cover
self._serialno = value
@property
@deprecated("1.0.0", "1.2.0")
def serialno_header(self): # pragma: no cover
return self._serialno_header
@serialno_header.setter
@deprecated("1.0.0", "1.2.0")
def serialno_header(self, value): # pragma: no cover
self._serialno_header = value
@property
@deprecated("1.0.0", "1.2.0", sign.fget)
def sign_mode(self): # pragma: no cover
return self.sign
@sign_mode.setter
@deprecated("1.0.0", "1.2.0", sign.fget)
def sign_mode(self, value): # pragma: no cover
self.sign = value
@property
def maxwidth(self):
"""get/set the maximum width of the table.
The width of the table is guaranteed to not exceed this value. If it
is not possible to print a given table with the width provided, this
value will automatically adjust.
"""
offset = (len(self.columns) - 1) * termwidth(self.columns.separator)
offset += termwidth(self.border.left)
offset += termwidth(self.border.right)
self._maxwidth = max(self._maxwidth, offset + len(self.columns))
return self._maxwidth
@maxwidth.setter
def maxwidth(self, value):
self._maxwidth = value
@property
@deprecated("1.0.0", "1.2.0", maxwidth.fget)
def max_table_width(self): # pragma: no cover
return self.maxwidth
@max_table_width.setter
@deprecated("1.0.0", "1.2.0", maxwidth.fget)
def max_table_width(self, value): # pragma: no cover
self.maxwidth = value
@property
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__len__,
details="Use 'len(self.columns)' instead.",
)
def column_count(self): # pragma: no cover
return len(self.columns)
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.width_exceed_policy.fget)
def width_exceed_policy(self): # pragma: no cover
return self.columns.width_exceed_policy
@width_exceed_policy.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.width_exceed_policy.fget)
def width_exceed_policy(self, value): # pragma: no cover
self.columns.width_exceed_policy = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.default_alignment.fget)
def default_alignment(self): # pragma: no cover
return self.columns.default_alignment
@default_alignment.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.default_alignment.fget)
def default_alignment(self, value): # pragma: no cover
self.columns.default_alignment = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.default_padding.fget)
def default_padding(self): # pragma: no cover
return self.columns.default_padding
@default_padding.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.default_padding.fget)
def default_padding(self, value): # pragma: no cover
self.columns.default_padding = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.width.fget)
def column_widths(self): # pragma: no cover
return self.columns.width
@column_widths.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.width.fget)
def column_widths(self, value): # pragma: no cover
self.columns.width = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.header.fget)
def column_headers(self): # pragma: no cover
return self.columns.header
@column_headers.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.header.fget)
def column_headers(self, value): # pragma: no cover
self.columns.header = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.alignment.fget)
def column_alignments(self): # pragma: no cover
return self.columns.alignment
@column_alignments.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.alignment.fget)
def column_alignments(self, value): # pragma: no cover
self.columns.alignment = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_left.fget)
def left_padding_widths(self): # pragma: no cover
return self.columns.padding_left
@left_padding_widths.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_left.fget)
def left_padding_widths(self, value): # pragma: no cover
self.columns.padding_left = value
@property
@deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_right.fget)
def right_padding_widths(self): # pragma: no cover
return self.columns.padding_right
@right_padding_widths.setter
@deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_right.fget)
def right_padding_widths(self, value): # pragma: no cover
self.columns.padding_right = value
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__getitem__,
details="Use 'BeautifulTable.{columns|rows}[key]' instead.",
)
def __getitem__(self, key): # pragma: no cover
return self.columns[key] if isinstance(key, basestring) else self.rows[key]
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__setitem__,
details="Use 'BeautifulTable.{columns|rows}[key]' instead.",
)
def __setitem__(self, key, value): # pragma: no cover
if isinstance(key, basestring):
self.columns[key] = value
else:
self.rows[key] = value
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__delitem__,
details="Use 'BeautifulTable.{columns|rows}[key]' instead.",
)
def __delitem__(self, key): # pragma: no cover
if isinstance(key, basestring):
del self.columns[key]
else:
del self.rows[key]
# *************************Properties End Here*************************
@deprecated(
"1.0.0",
"1.2.0",
BTColumnCollection.__getitem__,
details="Use 'BeautifulTable.columns[key]' instead.",
)
def get_column(self, key): # pragma: no cover
return self.columns[key]
@deprecated(
"1.0.0",
"1.2.0",
BTColumnHeader.__getitem__,
details="Use 'BeautifulTable.columns.header[key]' instead.",
)
def get_column_header(self, index): # pragma: no cover
return self.columns.header[index]
@deprecated(
"1.0.0",
"1.2.0",
BTColumnHeader.__getitem__,
details="Use 'BeautifulTable.columns.header.index(header)' instead.",
)
def get_column_index(self, header): # pragma: no cover
return self.columns.header.index(header)
@deprecated("1.0.0", "1.2.0", BTRowCollection.filter)
def filter(self, key): # pragma: no cover
return self.rows.filter(key)
@deprecated("1.0.0", "1.2.0", BTRowCollection.sort)
def sort(self, key, reverse=False): # pragma: no cover
self.rows.sort(key, reverse=reverse)
@deprecated("1.0.0", "1.2.0", BTRowCollection.reverse)
def reverse(self, value): # pragma: no cover
self.rows.reverse()
@deprecated("1.0.0", "1.2.0", BTRowCollection.pop)
def pop_row(self, index=-1): # pragma: no cover
return self.rows.pop(index)
@deprecated("1.0.0", "1.2.0", BTRowCollection.insert)
def insert_row(self, index, row): # pragma: no cover
return self.rows.insert(index, row)
@deprecated("1.0.0", "1.2.0", BTRowCollection.append)
def append_row(self, value): # pragma: no cover
self.rows.append(value)
@deprecated("1.0.0", "1.2.0", BTRowCollection.update)
def update_row(self, key, value): # pragma: no cover
self.rows.update(key, value)
@deprecated("1.0.0", "1.2.0", BTColumnCollection.pop)
def pop_column(self, index=-1): # pragma: no cover
return self.columns.pop(index)
@deprecated("1.0.0", "1.2.0", BTColumnCollection.insert)
def insert_column(self, index, header, column): # pragma: no cover
self.columns.insert(index, column, header)
@deprecated("1.0.0", "1.2.0", BTColumnCollection.append)
def append_column(self, header, column): # pragma: no cover
self.columns.append(column, header)
@deprecated("1.0.0", "1.2.0", BTColumnCollection.update)
def update_column(self, header, column): # pragma: no cover
self.columns.update(header, column)
[docs] def set_style(self, style):
"""Set the style of the table from a predefined set of styles.
Parameters
----------
style: Style
It can be one of the following:
* beautifultable.STYLE_DEFAULT
* beautifultable.STYLE_NONE
* beautifultable.STYLE_DOTTED
* beautifultable.STYLE_MYSQL
* beautifultable.STYLE_SEPARATED
* beautifultable.STYLE_COMPACT
* beautifultable.STYLE_MARKDOWN
* beautifultable.STYLE_RESTRUCTURED_TEXT
* beautifultable.STYLE_BOX
* beautifultable.STYLE_BOX_DOUBLED
* beautifultable.STYLE_BOX_ROUNDED
* beautifultable.STYLE_GRID
"""
if not isinstance(style, enums.Style):
allowed = (f"{type(self).__name__}.{i.name}" for i in enums.Style)
error_msg = "allowed values for style are: " + ", ".join(allowed)
raise ValueError(error_msg)
style_template = style.value
self.border.left = style_template.left_border_char
self.border.right = style_template.right_border_char
self.border.top = style_template.top_border_char
self.border.bottom = style_template.bottom_border_char
self.border.top_left = style_template.intersect_top_left
self.border.bottom_left = style_template.intersect_bottom_left
self.border.bottom_right = style_template.intersect_bottom_right
self.border.top_right = style_template.intersect_top_right
self.border.header_left = style_template.intersect_header_left
self.border.header_right = style_template.intersect_header_right
self.columns.header.separator = style_template.header_separator_char
self.columns.separator = style_template.column_separator_char
self.rows.separator = style_template.row_separator_char
self.border.top_junction = style_template.intersect_top_mid
self.border.left_junction = style_template.intersect_row_left
self.border.bottom_junction = style_template.intersect_bottom_mid
self.border.right_junction = style_template.intersect_row_right
self.columns.header.junction = style_template.intersect_header_mid
self.junction = style_template.intersect_row_mid
def _compute_width(self):
"""Calculate width of column automatically based on data."""
table_width = self._width
lpw, rpw = self.columns.padding_left, self.columns.padding_right
pad_widths = [(lpw[i] + rpw[i]) for i in range(len(self.columns))]
maxwidths = [0 for index in range(len(self.columns))]
offset = table_width - sum(self.columns.width) + sum(pad_widths)
self._maxwidth = max(self._maxwidth, offset + len(self.columns))
for index, header in enumerate(self.columns.header):
max_length = 0
for i in pre_process(
header, self.detect_numerics, self.precision, self.sign.value
).split("\n"):
output_str = pre_process(
i,
self.detect_numerics,
self.precision,
self.sign.value,
)
max_length = max(max_length, termwidth(output_str))
maxwidths[index] += max_length
for index, column in enumerate(zip(*self._data)):
max_length = maxwidths[index]
for i in column:
for j in pre_process(
i, self.detect_numerics, self.precision, self.sign.value
).split("\n"):
output_str = pre_process(
j,
self.detect_numerics,
self.precision,
self.sign.value,
)
max_length = max(max_length, termwidth(output_str))
maxwidths[index] = max_length
sum_ = sum(maxwidths)
desired_sum = self._maxwidth - offset
# Set flag for columns who are within their fair share
temp_sum = 0
flag = [0] * len(maxwidths)
for i, width in enumerate(maxwidths):
if width <= int(desired_sum / len(self.columns)):
temp_sum += width
flag[i] = 1
else:
# Allocate atleast 1 character width to the column
temp_sum += 1
avail_space = desired_sum - temp_sum
actual_space = sum_ - temp_sum
shrinked_columns = {}
# Columns which exceed their fair share should be shrinked based on
# how much space is left for the table
for i, width in enumerate(maxwidths):
self.columns.width[i] = width
if not flag[i]:
new_width = 1 + int((width - 1) * avail_space / actual_space)
if new_width < width:
self.columns.width[i] = new_width
shrinked_columns[new_width] = i
# Divide any remaining space among shrinked columns
if shrinked_columns:
extra = self._maxwidth - offset - sum(self.columns.width)
actual_space = sum(shrinked_columns)
if extra > 0:
for i, width in enumerate(sorted(shrinked_columns)):
index = shrinked_columns[width]
extra_width = int(width * extra / actual_space)
self.columns.width[i] += extra_width
if i == (len(shrinked_columns) - 1):
extra = self._maxwidth - offset - sum(self.columns.width)
self.columns.width[index] += extra
for i in range(len(self.columns)):
self.columns.width[i] += pad_widths[i]
@deprecated("1.0.0", "1.2.0", BTColumnCollection.padding.fget)
def set_padding_widths(self, pad_width): # pragma: no cover
self.columns.padding_left = pad_width
self.columns.padding_right = pad_width
@deprecated("1.0.0", "1.2.0")
def copy(self):
return copy.copy(self)
[docs] @deprecated_param("1.0.0", "1.2.0", "clear_metadata", "reset_columns")
def clear(self, reset_columns=False, **kwargs): # pragma: no cover
"""Clear the contents of the table.
Clear all rows of the table, and if specified clears all column
specific data.
Parameters
----------
reset_columns : bool, optional
If it is true(default False), all metadata of columns such as their
alignment, padding, width, etc. are also cleared and number of
columns is set to 0.
"""
kwargs.setdefault("clear_metadata", None)
if kwargs["clear_metadata"]:
reset_columns = kwargs["clear_metadata"]
self.rows.clear()
if reset_columns:
self.columns.clear()
def _get_horizontal_line(
self, char, intersect_left, intersect_mid, intersect_right, mask=None
):
"""Get a horizontal line for the table.
Internal method used to draw all horizontal lines in the table.
Column width should be set prior to calling this method. This method
detects intersection and handles it according to the values of
`intersect_*_*` attributes.
Parameters
----------
char : str
Character used to draw the line.
Returns
-------
str
String which will be printed as a line in the table.
"""
width = self._width
if mask is None:
mask = [True] * len(self.columns)
try:
line = list(char * (int(width / termwidth(char)) + 1))[:width]
except ZeroDivisionError:
line = [" "] * width
if len(line) == 0:
return ""
# Only if Special Intersection is enabled and horizontal line is
# visible
if not char.isspace():
# If left border is enabled and it is visible
visible_junc = not intersect_left.isspace()
if termwidth(self.border.left) > 0:
if not (self.border.left.isspace() and visible_junc):
length = min(
termwidth(self.border.left),
termwidth(intersect_left),
)
for i in range(length):
line[i] = intersect_left[i] if mask[0] else " "
visible_junc = not intersect_right.isspace()
# If right border is enabled and it is visible
if termwidth(self.border.right) > 0:
if not (self.border.right.isspace() and visible_junc):
length = min(
termwidth(self.border.right),
termwidth(intersect_right),
)
for i in range(length):
line[-i - 1] = intersect_right[-i - 1] if mask[-1] else " "
visible_junc = not intersect_mid.isspace()
# If column separator is enabled and it is visible
if termwidth(self.columns.separator):
if not (self.columns.separator.isspace() and visible_junc):
index = termwidth(self.border.left)
for i in range(len(self.columns) - 1):
if not mask[i]:
for j in range(self.columns.width[i]):
line[index + j] = " "
index += self.columns.width[i]
length = min(
termwidth(self.columns.separator),
termwidth(intersect_mid),
)
for j in range(length):
# TODO: we should also hide junctions based on mask
line[index + j] = (
intersect_mid[j] if (mask[i] or mask[i + 1]) else " "
)
index += termwidth(self.columns.separator)
return "".join(line)
def _get_top_border(self, *args, **kwargs):
return self._get_horizontal_line(
self.border.top,
self.border.top_left,
self.border.top_junction,
self.border.top_right,
*args,
**kwargs,
)
def _get_header_separator(self, *args, **kwargs):
return self._get_horizontal_line(
self.columns.header.separator,
self.border.header_left,
self.columns.header.junction,
self.border.header_right,
*args,
**kwargs,
)
def _get_row_separator(self, *args, **kwargs):
return self._get_horizontal_line(
self.rows.separator,
self.border.left_junction,
self.junction,
self.border.right_junction,
*args,
**kwargs,
)
def _get_bottom_border(self, *args, **kwargs):
return self._get_horizontal_line(
self.border.bottom,
self.border.bottom_left,
self.border.bottom_junction,
self.border.bottom_right,
*args,
**kwargs,
)
@property
def _width(self):
"""Get the actual width of the table as number of characters.
Column width should be set prior to calling this method.
Returns
-------
int
Width of the table as number of characters.
"""
if len(self.columns) == 0:
return 0
width = sum(self.columns.width)
width += (len(self.columns) - 1) * termwidth(self.columns.separator)
width += termwidth(self.border.left)
width += termwidth(self.border.right)
return width
@deprecated("1.0.0", "1.2.0", _width.fget)
def get_table_width(self): # pragma: no cover
return self._width
def _get_string(self, rows=None, append=False, recalculate_width=True):
row_header_visible = bool(
"".join(x if x is not None else "" for x in self.rows.header).strip()
) and (len(self.columns) > 0)
column_header_visible = bool(
"".join(x if x is not None else "" for x in self.columns.header).strip()
) and (len(self.rows) > 0 or rows is not None)
# Preparing table for printing serialno, row headers and column headers
if len(self.columns) > 0:
if self._serialno:
self.columns.insert(
0, range(1, len(self.rows) + 1), self._serialno_header
)
if row_header_visible:
self.columns.insert(0, self.rows.header)
if column_header_visible:
self.rows.insert(0, self.columns.header)
if (self.columns._auto_width and recalculate_width) or sum(
self.columns.width
) == 0:
self._compute_width()
try:
# Rendering the top border
if self.border.top:
yield self._get_top_border()
# Print column headers if not empty or only spaces
row_iterator = iter(self.rows)
if column_header_visible:
yield next(row_iterator)._get_string(
align=self.columns.header.alignment
)
if self.columns.header.separator:
yield self._get_header_separator()
# Printing rows
first_row_encountered = False
for i, row in enumerate(row_iterator):
if first_row_encountered and self.rows.separator:
yield self._get_row_separator()
first_row_encountered = True
content = to_unicode(row)
yield content
if rows is not None:
# Printing additional rows
prev_length = len(self.rows)
for i, row in enumerate(rows, start=1):
if first_row_encountered and self.rows.separator:
yield self._get_row_separator()
first_row_encountered = True
if self._serialno:
row.insert(0, prev_length + i)
if row_header_visible:
self.rows.append([None] + list(row))
else:
self.rows.append(row)
content = to_unicode(self.rows[-1])
if not append:
self.rows.pop()
yield content
# Rendering the bottom border
if self.border.bottom:
yield self._get_bottom_border()
except Exception:
raise
finally:
# Cleanup
if column_header_visible:
self.rows.pop(0)
if row_header_visible:
self.columns.pop(0)
if len(self.columns) > 0:
if self._serialno:
self.columns.pop(0)
return
[docs] def stream(self, rows, append=False):
"""Get a generator for the table.
This should be used in cases where data takes time to retrieve and it
is required to be displayed as soon as possible. Any existing rows in
the table shall also be returned. It is essential that atleast one of
column header, width or existing rows set before calling this method.
Parameters
----------
rows : iterable
A generator which yields one row at a time.
append : bool, optional
If rows should also be appended to the table.(Default False)
Returns
-------
iterable:
string representation of the table as a generators
"""
for line in self._get_string(rows, append=append, recalculate_width=False):
yield line
@deprecated("1.0.0", "1.2.0", str)
def get_string(self):
return str(self)
[docs] def to_csv(self, file_name, *args, **kwargs):
"""Export table to CSV format.
Parameters
----------
file_name : str
Path to CSV file.
"""
if not isinstance(file_name, str):
raise ValueError(
f"Expected 'file_name' to be string, got {type(file_name).__name__}"
)
with open(file_name, mode="wt", newline="") as csv_file:
csv_writer = csv.writer(csv_file, *args, **kwargs)
if bool(
"".join(x if x is not None else "" for x in self.columns.header).strip()
):
csv_writer.writerow(self.columns.header)
csv_writer.writerows(self.rows)
[docs] def from_csv(self, file_name, header=True, **kwargs):
"""Create table from CSV file.
Parameters
----------
file_name : str
Path to CSV file.
header : bool, optional
Whether First row in CSV file should be parsed as table header.
Raises
------
ValueError
If `file_name` is not str type.
FileNotFoundError
If `file_name` is not valid path to file.
"""
if not isinstance(file_name, str):
raise ValueError(
f"Expected 'file_name' to be string, got {type(file_name).__name__}"
)
with open(file_name, mode="rt", newline="") as csv_file:
csv_reader = csv.reader(csv_file, **kwargs)
if header:
self.columns.header = next(csv_reader)
for row in csv_reader:
self.rows.append(row)
return self
[docs] def to_df(self):
"""Export table to dataframe.
This method requires that you have `pandas` already installed in your machine.
Returns
-------
pandas.Dataframe:
The exported dataframe
"""
try:
import pandas as pd
except ImportError:
warnings.warn(
"This method requires that 'pandas' is installed.", RuntimeWarning
)
raise
# If there are column headers then it will act as a column of dataframe
headers = list(self.columns.header)
if headers.count(None) == len(headers):
headers = None
# If there are row headers then it will act as an Index
index = list(self.rows.header)
if index.count(None) == len(index):
index = None
return pd.DataFrame(
[list(row) for row in self.rows], columns=headers, index=index
)
[docs] def from_df(self, df):
"""Import table from dataframe.
Parameters
----------
df : pandas.Dataframe
input dataframe
"""
data = df.to_dict()
# Dataframe columns will act as a column headers
headers = list(data.keys())
# Index of dataframe will act as a row headers
row_header = list(df.index)
for header in headers:
self.columns.append(
[data[header][indx] for indx in row_header], header=str(header)
)
self.rows.header = row_header
return self