"""This module provides BeautifulTable class
It is intended for printing Tabular data to terminals.
Example
-------
>>> from beautifultable import BeautifulTable
>>> table = BeautifulTable()
>>> table.column_headers = ['1st column', '2nd column']
>>> for i in range(5):
... table.append_row([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 operator
from . import enums
from .utils import get_output_str, raise_suppressed, termwidth, deprecation
from .rows import RowData, HeaderData
from .meta import AlignmentMetaData, PositiveIntegerMetaData
from .compat import basestring, Iterable, to_unicode
__all__ = ['BeautifulTable']
[docs]class BeautifulTable(object):
"""Utility Class to print data in tabular format to terminal.
The instance attributes can be used to customize the look of the
table. To disable a behaviour, just set its corresponding attribute
to an empty string. For example, if Top border should not be drawn,
set `top_border_char` to ''.
Parameters
----------
max_width: 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).
Attributes
----------
left_border_char : str
Character used to draw the left border.
right_border_char : str
Character used to draw the right border.
top_border_char : str
Character used to draw the top border.
bottom_border_char : str
Character used to draw the bottom border.
header_separator_char : str
Character used to draw the line seperating Header from data.
row_separator_char : str
Character used to draw the line seperating two rows.
column_separator_char : str
Character used to draw the line seperating two columns.
intersection_char : str
Character used to draw intersection of a vertical and horizontal
line. Disabling it just draws the horizontal line char in it's place.
(DEPRECATED).
intersect_top_left : str
Left most character of the top border.
intersect_top_mid : str
Intersection character for top border.
intersect_top_right : str
Right most character of the top border.
intersect_header_left : str
Left most character of the header separator.
intersect_header_mid : str
Intersection character for header separator.
intersect_header_right : str
Right most character of the header separator.
intersect_row_left : str
Left most character of the row separator.
intersect_row_mid : str
Intersection character for row separator.
intersect_row_right : str
Right most character of the row separator.
intersect_bottom_left : str
Left most character of the bottom border.
intersect_bottom_mid : str
Intersection character for bottom border.
intersect_bottom_right : str
Right most character of the bottom border.
numeric_precision : int
All float values will have maximum number of digits after the decimal,
capped by this value(Default 3).
serialno : bool
Whether automatically generated serial number should be printed for
each row(Default False).
serialno_header : str
The header of the autogenerated serial number column. This value is
only used if serialno is True(Default SN).
detect_numerics : bool
Whether numeric strings should be automatically detected(Default True).
"""
def __init__(self, max_width=80,
default_alignment=enums.ALIGN_CENTER,
default_padding=1):
self.set_style(enums.STYLE_DEFAULT)
self.numeric_precision = 3
self.serialno = False
self.serialno_header = "SN"
self.detect_numerics = True
self._column_count = 0
self._sign_mode = enums.SM_MINUS
self._width_exceed_policy = enums.WEP_WRAP
self._column_pad = " "
self.default_alignment = default_alignment
self.default_padding = default_padding
self.max_table_width = max_width
self._initialize_table(0)
self._table = []
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 and not isinstance(value, basestring):
value_type = type(value).__name__
raise TypeError(("Expected {attr} to be of type 'str', "
"got '{attr_type}'").format(attr=name,
attr_type=value_type))
super(BeautifulTable, self).__setattr__(name, value)
# ****************************Properties Begin Here****************************
@property
def column_count(self):
"""Get the number of columns in the table(read only)"""
return self._column_count
@property
def intersection_char(self): # pragma : no cover
"""Character used to draw intersection of perpendicular lines.
Disabling it just draws the horizontal line char in it's place.
This attribute is deprecated. Use specific intersect_*_* attribute.
"""
deprecation("'intersection_char' is deprecated, Use specific "
"`intersect_*_*` attribute instead")
return self.intersect_top_left
@intersection_char.setter
def intersection_char(self, value): # pragma : no cover
deprecation("'intersection_char' is deprecated, Use specific "
"`intersect_*_*` attributes instead")
self.intersect_top_left = value
self.intersect_top_mid = value
self.intersect_top_right = value
self.intersect_header_left = value
self.intersect_header_mid = value
self.intersect_header_right = value
self.intersect_row_left = value
self.intersect_row_mid = value
self.intersect_row_right = value
self.intersect_bottom_left = value
self.intersect_bottom_mid = value
self.intersect_bottom_right = value
@property
def sign_mode(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_mode
@sign_mode.setter
def sign_mode(self, value):
if not isinstance(value, enums.SignMode):
allowed = ("{}.{}".format(type(self).__name__, i.name)
for i in enums.SignMode)
error_msg = ("allowed values for sign_mode are: "
+ ', '.join(allowed))
raise ValueError(error_msg)
self._sign_mode = value
@property
def width_exceed_policy(self):
"""Attribute to control how exceeding column width should be handled.
It can be one of the following:
============================ =========================================
Option Meaning
============================ =========================================
beautifulbable.WEP_WRAP An item is wrapped so every line fits
within it's column width.
beautifultable.WEP_STRIP An item is stripped to fit in it's
column.
beautifultable.WEP_ELLIPSIS An item is stripped to fit in it's
column and appended with ...(Ellipsis).
============================ =========================================
"""
return self._width_exceed_policy
@width_exceed_policy.setter
def width_exceed_policy(self, value):
if not isinstance(value, enums.WidthExceedPolicy):
allowed = ("{}.{}".format(type(self).__name__, i.name)
for i in enums.WidthExceedPolicy)
error_msg = ("allowed values for width_exceed_policy are: "
+ ', '.join(allowed))
raise ValueError(error_msg)
self._width_exceed_policy = value
@property
def default_alignment(self):
"""Attribute to control the alignment of newly created columns.
It can be one of the following:
============================ =========================================
Option Meaning
============================ =========================================
beautifultable.ALIGN_LEFT New columns are left aligned.
beautifultable.ALIGN_CENTER New columns are center aligned.
beautifultable.ALIGN_RIGHT New columns are right aligned.
============================ =========================================
"""
return self._default_alignment
@default_alignment.setter
def default_alignment(self, value):
if not isinstance(value, enums.Alignment):
allowed = ("{}.{}".format(type(self).__name__, i.name)
for i in enums.Alignment)
error_msg = ("allowed values for default_alignment are: "
+ ', '.join(allowed))
raise ValueError(error_msg)
self._default_alignment = value
@property
def default_padding(self):
"""Initial value for Left and Right padding widths for new columns."""
return self._default_padding
@default_padding.setter
def default_padding(self, value):
if not isinstance(value, int):
raise TypeError("padding must be an integer")
elif value <= 0:
raise ValueError("padding must be more than 0")
else:
self._default_padding = value
@property
def column_widths(self):
"""get/set width for the columns of the table.
Width of the column specifies the max number of characters
a column can contain. Larger characters are handled according to
the value of `width_exceed_policy`.
"""
return self._column_widths
@column_widths.setter
def column_widths(self, value):
width = self._validate_row(value)
self._column_widths = PositiveIntegerMetaData(self, width)
@property
def column_headers(self):
"""get/set titles for the columns of the table.
It can be any iterable having all memebers an instance of `str`.
"""
return self._column_headers
@column_headers.setter
def column_headers(self, value):
header = self._validate_row(value)
for i in header:
if not isinstance(i, basestring):
raise TypeError(("Headers should be of type 'str', "
"not {}").format(type(i)))
self._column_headers = HeaderData(self, header)
@property
def column_alignments(self):
"""get/set alignment of the columns of the table.
It can be any iterable containing only the following:
* beautifultable.ALIGN_LEFT
* beautifultable.ALIGN_CENTER
* beautifultable.ALIGN_RIGHT
"""
return self._column_alignments
@column_alignments.setter
def column_alignments(self, value):
alignment = self._validate_row(value)
self._column_alignments = AlignmentMetaData(self, alignment)
@property
def left_padding_widths(self):
"""get/set width for left padding of the columns of the table.
Left Width of the padding specifies the number of characters
on the left of a column reserved for padding. By Default It is 1.
"""
return self._left_padding_widths
@left_padding_widths.setter
def left_padding_widths(self, value):
pad_width = self._validate_row(value)
self._left_padding_widths = PositiveIntegerMetaData(self, pad_width)
@property
def right_padding_widths(self):
"""get/set width for right padding of the columns of the table.
Right Width of the padding specifies the number of characters
on the rigth of a column reserved for padding. By default It is 1.
"""
return self._right_padding_widths
@right_padding_widths.setter
def right_padding_widths(self, value):
pad_width = self._validate_row(value)
self._right_padding_widths = PositiveIntegerMetaData(self, pad_width)
@property
def max_table_width(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 = ((self._column_count - 1)
* termwidth(self.column_separator_char))
offset += termwidth(self.left_border_char)
offset += termwidth(self.right_border_char)
self._max_table_width = max(self._max_table_width,
offset + self._column_count)
return self._max_table_width
@max_table_width.setter
def max_table_width(self, value):
self._max_table_width = value
# *****************************Properties End Here*****************************
def _initialize_table(self, column_count):
"""Sets the column count of the table.
This method is called to set the number of columns for the first time.
Parameters
----------
column_count : int
number of columns in the table
"""
header = [''] * column_count
alignment = [self.default_alignment] * column_count
width = [0] * column_count
padding = [self.default_padding] * column_count
self._column_count = column_count
self._column_headers = HeaderData(self, header)
self._column_alignments = AlignmentMetaData(self, alignment)
self._column_widths = PositiveIntegerMetaData(self, width)
self._left_padding_widths = PositiveIntegerMetaData(self, padding)
self._right_padding_widths = PositiveIntegerMetaData(self, padding)
def _validate_row(self, value, init_table_if_required=True):
# TODO: Rename this method
# str is also an iterable but it is not a valid row, so
# an extra check is required for str
if not isinstance(value, Iterable) or isinstance(value, basestring):
raise TypeError("parameter must be an iterable")
row = list(value)
if init_table_if_required and self._column_count == 0:
self._initialize_table(len(row))
if len(row) != self._column_count:
raise ValueError(("'Expected iterable of length {}, "
"got {}").format(self._column_count, len(row)))
return row
def __getitem__(self, key):
"""Get a row, or a column, or a new table by slicing.
Parameters
----------
key : int, slice, str
If key is an `int`, returns a row.
If key is an `str`, returns iterator to a column with header `key`.
If key is a slice object, returns a new table sliced according to
rows.
Raises
------
TypeError
If key is not of type int, slice or str.
IndexError
If `int` key is out of range.
KeyError
If `str` key is not found in headers.
"""
if isinstance(key, slice):
new_table = copy.copy(self)
# Every child of BaseRow class needs to be reassigned so that
# They contain reference of the new table rather than the old
# This was a cause of a nasty bug once.
new_table.column_headers = self.column_headers
new_table.column_alignments = self.column_alignments
new_table.column_widths = self.column_widths
new_table.left_padding_widths = self.left_padding_widths
new_table.right_padding_widths = self.left_padding_widths
new_table._table = []
for row in self._table[key]:
new_table.append_row(row)
return new_table
elif isinstance(key, int):
return self._table[key]
elif isinstance(key, basestring):
return self.get_column(key)
else:
raise TypeError(("table indices must be integers, strings or "
"slices, not {}").format(type(key).__name__))
def __delitem__(self, key):
"""Delete a row, or a column, or multiple rows by slicing.
Parameters
----------
key : int, slice, str
If key is an `int`, deletes a row.
If key is a slice object, deletes multiple rows.
If key is an `str`, delete the first column with heading `key`
Raises
------
TypeError
If key is not of type int, slice or str.
IndexError
If `int` key is out of range.
KeyError
If `str` key is not found in headers.
"""
if isinstance(key, int) or isinstance(key, slice):
del self._table[key]
elif isinstance(key, basestring):
return self.pop_column(key)
else:
raise TypeError(("table indices must be integers, strings or "
"slices, not {}").format(type(key).__name__))
def __setitem__(self, key, value):
"""Update a row, or a column, or multiple rows by slicing.
Parameters
----------
key : int, slice, str
If key is an `int`, updates a row.
If key is an `str`, appends `column` to the list with header as
`key`.
If key is a slice object, updates multiple rows according to slice
rules.
Raises
------
TypeError
If key is not of type int, slice or str.
IndexError
If `int` key is out of range.
"""
if isinstance(key, (int, slice)):
self.update_row(key, value)
elif isinstance(key, basestring):
self.update_column(key, value)
else:
raise TypeError(("table indices must be integers, strings or "
"slices, not {}").format(type(key).__name__))
def __len__(self):
return len(self._table)
def __contains__(self, key):
if isinstance(key, basestring):
return key in self._column_headers
elif isinstance(key, Iterable):
return key in self._table
else:
raise TypeError(("'key' must be str or Iterable, "
"not {}").format(type(key).__name__))
def __iter__(self):
return iter(self._table)
def __next__(self):
return next(self._table)
def __repr__(self):
return repr(self._table)
def __str__(self):
return self.get_string()
[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 = ("{}.{}".format(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.left_border_char = style_template.left_border_char
self.right_border_char = style_template.right_border_char
self.top_border_char = style_template.top_border_char
self.bottom_border_char = style_template.bottom_border_char
self.header_separator_char = style_template.header_separator_char
self.column_separator_char = style_template.column_separator_char
self.row_separator_char = style_template.row_separator_char
self.intersect_top_left = style_template.intersect_top_left
self.intersect_top_mid = style_template.intersect_top_mid
self.intersect_top_right = style_template.intersect_top_right
self.intersect_header_left = style_template.intersect_header_left
self.intersect_header_mid = style_template.intersect_header_mid
self.intersect_header_right = style_template.intersect_header_right
self.intersect_row_left = style_template.intersect_row_left
self.intersect_row_mid = style_template.intersect_row_mid
self.intersect_row_right = style_template.intersect_row_right
self.intersect_bottom_left = style_template.intersect_bottom_left
self.intersect_bottom_mid = style_template.intersect_bottom_mid
self.intersect_bottom_right = style_template.intersect_bottom_right
def _calculate_column_widths(self):
"""Calculate width of column automatically based on data."""
table_width = self.get_table_width()
lpw, rpw = self._left_padding_widths, self._right_padding_widths
pad_widths = [(lpw[i] + rpw[i]) for i in range(self._column_count)]
max_widths = [0 for index in range(self._column_count)]
offset = table_width - sum(self._column_widths) + sum(pad_widths)
self._max_table_width = max(self._max_table_width,
offset + self._column_count)
for index, column in enumerate(zip(*self._table)):
max_length = 0
for i in column:
for j in to_unicode(i).split('\n'):
output_str = get_output_str(j, self.detect_numerics,
self.numeric_precision,
self.sign_mode.value)
max_length = max(max_length, termwidth(output_str))
for i in to_unicode(self._column_headers[index]).split('\n'):
output_str = get_output_str(i, self.detect_numerics,
self.numeric_precision,
self.sign_mode.value)
max_length = max(max_length, termwidth(output_str))
max_widths[index] += max_length
sum_ = sum(max_widths)
desired_sum = self._max_table_width - offset
# Set flag for columns who are within their fair share
temp_sum = 0
flag = [0] * len(max_widths)
for i, width in enumerate(max_widths):
if width <= int(desired_sum / self._column_count):
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(max_widths):
self.column_widths[i] = width
if not flag[i]:
new_width = 1 + int((width-1) * avail_space / actual_space)
if new_width < width:
self.column_widths[i] = new_width
shrinked_columns[new_width] = i
# Divide any remaining space among shrinked columns
if shrinked_columns:
extra = (self._max_table_width
- offset
- sum(self.column_widths))
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.column_widths[i] += extra_width
if i == (len(shrinked_columns) - 1):
extra = (self._max_table_width
- offset
- sum(self.column_widths))
self.column_widths[index] += extra
for i in range(self.column_count):
self.column_widths[i] += pad_widths[i]
def auto_calculate_width(self): # pragma : no cover
deprecation("'auto_calculate_width()' is deprecated")
self._calculate_column_widths()
[docs] def set_padding_widths(self, pad_width):
"""Set width for left and rigth padding of the columns of the table.
Parameters
----------
pad_width : array_like
pad widths for the columns.
"""
self.left_padding_widths = pad_width
self.right_padding_widths = pad_width
[docs] def sort(self, key, reverse=False):
"""Stable sort of the table *IN-PLACE* with respect to a column.
Parameters
----------
key: int, str
index or header of the column. Normal list rules apply.
reverse : bool
If `True` then table is sorted as if each comparison was reversed.
"""
if isinstance(key, int):
index = key
elif isinstance(key, basestring):
index = self.get_column_index(key)
else:
raise TypeError("'key' must either be 'int' or 'str'")
self._table.sort(key=operator.itemgetter(index), reverse=reverse)
[docs] def copy(self):
"""Return a shallow copy of the table.
Returns
-------
BeautifulTable:
shallow copy of the BeautifulTable instance.
"""
return self[:]
[docs] def get_column_header(self, index):
"""Get header of a column from it's index.
Parameters
----------
index: int
Normal list rules apply.
"""
return self._column_headers[index]
[docs] def get_column_index(self, header):
"""Get index of a column from it's header.
Parameters
----------
header: str
header of the column.
Raises
------
ValueError:
If no column could be found corresponding to `header`.
"""
try:
index = self._column_headers.index(header)
return index
except ValueError:
raise_suppressed(KeyError(("'{}' is not a header for any "
"column").format(header)))
[docs] def get_column(self, key):
"""Return an iterator to a column.
Parameters
----------
key : int, str
index of the column, or the header of the column.
If index is specified, then normal list rules apply.
Raises
------
TypeError:
If key is not of type `int`, or `str`.
Returns
-------
iter:
Iterator to the specified column.
"""
if isinstance(key, int):
index = key
elif isinstance(key, basestring):
index = self.get_column_index(key)
else:
raise TypeError(("key must be an int or str, "
"not {}").format(type(key).__name__))
return iter(map(operator.itemgetter(index), self._table))
[docs] def reverse(self):
"""Reverse the table row-wise *IN PLACE*."""
self._table.reverse()
[docs] def pop_row(self, index=-1):
"""Remove and return row at index (default last).
Parameters
----------
index : int
index of the row. Normal list rules apply.
"""
row = self._table.pop(index)
return row
[docs] def pop_column(self, index=-1):
"""Remove and return row at index (default last).
Parameters
----------
index : int, str
index of the column, or the header of the column.
If index is specified, then normal list rules apply.
Raises
------
TypeError:
If index is not an instance of `int`, or `str`.
IndexError:
If Table is empty.
"""
if isinstance(index, int):
pass
elif isinstance(index, basestring):
index = self.get_column_index(index)
else:
raise TypeError(("column index must be an integer or a string, "
"not {}").format(type(index).__name__))
if self._column_count == 0:
raise IndexError("pop from empty table")
if self._column_count == 1:
# This is the last column. So we should clear the table to avoid
# empty rows
self.clear(clear_metadata=True)
else:
# Not the last column. safe to pop from row
self._column_count -= 1
self._column_alignments._pop(index)
self._column_widths._pop(index)
self._left_padding_widths._pop(index)
self._right_padding_widths._pop(index)
self._column_headers._pop(index)
for row in self._table:
row._pop(index)
[docs] def insert_row(self, index, row):
"""Insert a row before index in the table.
Parameters
----------
index : int
List index rules apply
row : iterable
Any iterable of appropriate length.
Raises
------
TypeError:
If `row` is not an iterable.
ValueError:
If size of `row` is inconsistent with the current number
of columns.
"""
row = self._validate_row(row)
row_obj = RowData(self, row)
self._table.insert(index, row_obj)
[docs] def append_row(self, row):
"""Append a row to end of the table.
Parameters
----------
row : iterable
Any iterable of appropriate length.
"""
self.insert_row(len(self._table), row)
[docs] def update_row(self, key, value):
"""Update a column named `header` in the table.
If length of column is smaller than number of rows, lets say
`k`, only the first `k` values in the column is updated.
Parameters
----------
key : int or slice
index of the row, or a slice object.
value : iterable
If an index is specified, `value` should be an iterable
of appropriate length. Instead if a slice object is
passed as key, value should be an iterable of rows.
Raises
------
IndexError:
If index specified is out of range.
TypeError:
If `value` is of incorrect type.
ValueError:
If length of row does not matches number of columns.
"""
if isinstance(key, int):
row = self._validate_row(value, init_table_if_required=False)
row_obj = RowData(self, row)
self._table[key] = row_obj
elif isinstance(key, slice):
row_obj_list = []
for row in value:
row_ = self._validate_row(row, init_table_if_required=True)
row_obj_list.append(RowData(self, row_))
self._table[key] = row_obj_list
else:
raise TypeError("key must be an integer or a slice object")
[docs] def update_column(self, header, column):
"""Update a column named `header` in the table.
If length of column is smaller than number of rows, lets say
`k`, only the first `k` values in the column is updated.
Parameters
----------
header : str
Header of the column
column : iterable
Any iterable of appropriate length.
Raises
------
TypeError:
If length of `column` is shorter than number of rows.
ValueError:
If no column exists with title `header`.
"""
index = self.get_column_index(header)
if not isinstance(header, basestring):
raise TypeError("header must be of type str")
for row, new_item in zip(self._table, column):
row[index] = new_item
[docs] def insert_column(self, index, header, column):
"""Insert a column before `index` in the table.
If length of column is bigger than number of rows, lets say
`k`, only the first `k` values of `column` is considered.
If column is shorter than 'k', ValueError is raised.
Note that Table remains in consistent state even if column
is too short. Any changes made by this method is rolled back
before raising the exception.
Parameters
----------
index : int
List index rules apply.
header : str
Title of the column.
column : iterable
Any iterable of appropriate length.
Raises
------
TypeError:
If `header` is not of type `str`.
ValueError:
If length of `column` is shorter than number of rows.
"""
if self._column_count == 0:
self.column_headers = HeaderData(self, [header])
self._table = [RowData(self, [i]) for i in column]
else:
if not isinstance(header, basestring):
raise TypeError("header must be of type str")
column_length = 0
for i, (row, new_item) in enumerate(zip(self._table, column)):
row._insert(index, new_item)
column_length = i
if column_length == len(self._table) - 1:
self._column_count += 1
self._column_headers._insert(index, header)
self._column_alignments._insert(index, self.default_alignment)
self._column_widths._insert(index, 0)
self._left_padding_widths._insert(index, self.default_padding)
self._right_padding_widths._insert(index, self.default_padding)
else:
# Roll back changes so that table remains in consistent state
for j in range(column_length, -1, -1):
self._table[j]._pop(index)
raise ValueError(("length of 'column' should be atleast {}, "
"got {}").format(len(self._table),
column_length + 1))
[docs] def append_column(self, header, column):
"""Append a column to end of the table.
Parameters
----------
header : str
Title of the column
column : iterable
Any iterable of appropriate length.
"""
self.insert_column(self._column_count, header, column)
[docs] def clear(self, clear_metadata=False):
"""Clear the contents of the table.
Clear all rows of the table, and if specified clears all column
specific data.
Parameters
----------
clear_metadata : 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.
"""
# Cannot use clear method to support Python 2.7
del self._table[:]
if clear_metadata:
self._initialize_table(0)
def _get_horizontal_line(self, char, intersect_left,
intersect_mid, intersect_right):
"""Get a horizontal line for the table.
Internal method used to actually get 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 the Top border of the table.
"""
width = self.get_table_width()
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.left_border_char) > 0:
if not (self.left_border_char.isspace() and visible_junc):
length = min(termwidth(self.left_border_char),
termwidth(intersect_left))
for i in range(length):
line[i] = intersect_left[i]
visible_junc = not intersect_right.isspace()
# If right border is enabled and it is visible
if termwidth(self.right_border_char) > 0:
if not (self.right_border_char.isspace() and visible_junc):
length = min(termwidth(self.right_border_char),
termwidth(intersect_right))
for i in range(length):
line[-i-1] = intersect_right[-i-1]
visible_junc = not intersect_mid.isspace()
# If column separator is enabled and it is visible
if termwidth(self.column_separator_char):
if not (self.column_separator_char.isspace() and visible_junc):
index = termwidth(self.left_border_char)
for i in range(self._column_count-1):
index += (self._column_widths[i])
length = min(termwidth(self.column_separator_char),
termwidth(intersect_mid))
for i in range(length):
line[index+i] = intersect_mid[i]
index += termwidth(self.column_separator_char)
return ''.join(line)
def _get_top_border(self):
return self._get_horizontal_line(self.top_border_char,
self.intersect_top_left,
self.intersect_top_mid,
self.intersect_top_right)
[docs] def get_top_border(self): # pragma : no cover
"""Get the Top border of table.
Column width should be set prior to calling this method.
Returns
-------
str
String which will be printed as the Top border of the table.
"""
deprecation("'get_top_border()' is deprecated")
return self._get_top_border()
def _get_header_separator(self):
return self._get_horizontal_line(self.header_separator_char,
self.intersect_header_left,
self.intersect_header_mid,
self.intersect_header_right)
def _get_row_separator(self):
return self._get_horizontal_line(self.row_separator_char,
self.intersect_row_left,
self.intersect_row_mid,
self.intersect_row_right)
[docs] def get_row_separator(self): # pragma : no cover
"""Get the Row separator of table.
Column width should be set prior to calling this method.
Returns
-------
str
String which will be printed as Row separator of the table.
"""
deprecation("'get_row_separator()' is deprecated")
return self._get_row_separator()
def _get_bottom_border(self):
return self._get_horizontal_line(self.bottom_border_char,
self.intersect_bottom_left,
self.intersect_bottom_mid,
self.intersect_bottom_right)
[docs] def get_bottom_border(self): # pragma : no cover
"""Get the Bottom border of table.
Column width should be set prior to calling this method.
Returns
-------
str
String which will be printed as Bottom border of the table.
"""
deprecation("'get_bottom_border()' is deprecated")
return self._get_bottom_border()
[docs] def get_table_width(self):
"""Get the 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 self.column_count == 0:
return 0
width = sum(self._column_widths)
width += ((self._column_count - 1)
* termwidth(self.column_separator_char))
width += termwidth(self.left_border_char)
width += termwidth(self.right_border_char)
return width
[docs] def get_string(self, recalculate_width=True):
"""Get the table as a String.
Parameters
----------
recalculate_width : bool, optional
If width for each column should be recalculated(default True).
Note that width is always calculated if it wasn't set
explicitly when this method is called for the first time ,
regardless of the value of `recalculate_width`.
Returns
-------
str:
Table as a string.
"""
# Empty table. returning empty string.
if len(self._table) == 0:
return ''
if self.serialno and self.column_count > 0:
self.insert_column(0, self.serialno_header,
range(1, len(self) + 1))
# Should widths of column be recalculated
if recalculate_width or sum(self._column_widths) == 0:
self._calculate_column_widths()
string_ = []
# Drawing the top border
if self.top_border_char:
string_.append(
self._get_top_border())
# Print headers if not empty or only spaces
if ''.join(self._column_headers).strip():
headers = to_unicode(self._column_headers)
string_.append(headers)
if self.header_separator_char:
string_.append(
self._get_header_separator())
# Printing rows
first_row_encountered = False
for row in self._table:
if first_row_encountered and self.row_separator_char:
string_.append(
self._get_row_separator())
first_row_encountered = True
content = to_unicode(row)
string_.append(content)
# Drawing the bottom border
if self.bottom_border_char:
string_.append(
self._get_bottom_border())
if self.serialno and self.column_count > 0:
self.pop_column(0)
return '\n'.join(string_)