Hrabal/TemPy

View on GitHub
tempy/widgets/tempytable.py

Summary

Maintainability
B
6 hrs
Test Coverage
# -*- coding: utf-8 -*-
"""
@author: Federico Cerchiari <federicocerchiari@gmail.com>
Table Widget
"""
from copy import copy
from itertools import zip_longest

import tempy.tags as tags
from ..tools import AdjustableList
from ..exceptions import WidgetDataError


class TempyTable(tags.Table):
    """Table widget.
    Creates a simple table structure using the give data, or an empty table of the given size.self
    params:
    data: an iterable of iterables in this form [[col1, col2, col3], [col1, col2, col3]]
    rows, columns: size of the table if no data is given
    head: if True adds the table header using the first data row
    foot: if True add a table footer using the last data row
    caption: adds the caption to the table
    """
    __tag = tags.Table._Table__tag

    def __init__(self, rows=0, cols=0, data=None, **kwargs):
        caption = kwargs.pop("caption", None)
        head = kwargs.pop("head", False)
        foot = kwargs.pop("foot", False)
        super().__init__(**kwargs)
        self(body=tags.Tbody())
        # Initialize empty data structure if data is not given
        if not data:
            data = [
                [None for _ in range(cols)] for _ in range(rows + sum((head, foot)))
            ]
        else:
            rows = len(data)
            cols = max(map(len, data))
        table_data = copy(data)
        if caption:
            self.make_caption(caption)
        if head and rows > 0:
            self.make_header(table_data.pop(0))
        if foot and rows > 0:
            self.make_footer(table_data.pop())
        if data:
            self.populate(table_data, resize_x=True)

    def _check_row_size(self, row):
        try:
            row_length = len(row)
        except TypeError:
            row_length = row
        if self.body.childs and max(map(len, self.body)) < row_length:
            raise WidgetDataError(self, "The given data has more columns than the table column size.")

    def populate(self, data, resize_x=True, normalize=True):
        """Adds/Replace data in the table.
        data: an iterable of iterables in the form [[col1, col2, col3], [col1, col2, col3]]
        resize_x: if True, changes the x-size of the table according to the given data.
            If False and data have dimensions different from the existing table structure a WidgetDataError is raised.
        normalize: if True all the rows will have the same number of columns, if False, data structure is followed.
        """
        if data is None:
            raise WidgetDataError(
                self,
                "Parameter data should be non-None, to empty the table use TempyTable.clear() or "
                "pass an empty list.",
            )
        data = copy(data)
        if not self.body:
            self(body=tags.Tbody())
        self.clear()

        max_data_x = max(map(len, data))
        if not resize_x:
            self._check_row_size(max_data_x)

        for t_row, d_row in zip_longest(self.body, data):
            if not d_row:
                t_row.remove()
            else:
                if not t_row:
                    t_row = tags.Tr().append_to(self.body)
                if normalize:
                    d_row = AdjustableList(d_row).ljust(max_data_x, None)
                for t_cell, d_cell in zip_longest(t_row, d_row):
                    if not t_cell and resize_x:
                        t_cell = tags.Td().append_to(t_row)
                    t_cell.empty()
                    if d_cell is not None:
                        t_cell(d_cell)
        return self

    def clear(self):
        return self.body.empty()

    def add_row(self, row_data, resize_x=True):
        """Adds a row at the end of the table"""
        if not resize_x:
            self._check_row_size(row_data)
        self.body(tags.Tr()(tags.Td()(cell) for cell in row_data))
        return self

    def pop_row(self, idr=None, tags=False):
        """Pops a row, default the last"""
        idr = idr if idr is not None else len(self.body) - 1
        row = self.body.pop(idr)
        return row if tags else [cell.childs[0] for cell in row]

    def pop_cell(self, idy=None, idx=None, tags=False):
        """Pops a cell, default the last of the last row"""
        idy = idy if idy is not None else len(self.body) - 1
        idx = idx if idx is not None else len(self.body[idy]) - 1
        cell = self.body[idy].pop(idx)
        return cell if tags else cell.childs[0]

    def _make_table_part(self, part, data):
        part_tag, inner_tag = {"header": (tags.Thead, tags.Th), "footer": (tags.Tfoot, tags.Td)}.get(part)
        part_instance = part_tag().append_to(self)
        if not hasattr(self, part):
            setattr(self, part, part_instance)
        return part_instance(tags.Tr()(inner_tag()(col) for col in data))

    def make_header(self, head):
        """Makes the header row from the given data."""
        self._make_table_part("header", head)

    def make_footer(self, footer):
        """Makes the footer row from the given data."""
        self._make_table_part("footer", footer)

    def make_caption(self, caption):
        """Adds/Substitutes the table's caption."""
        if not hasattr(self, "caption"):
            self(caption=tags.Caption())
        return self.caption.empty()(caption)

    def _iter_rows(self, col_index):
        for row in self.body.childs:
            if self.is_col_within_bounds(col_index, row) and row.childs[col_index].childs:
                yield row

    def col_class(self, css_class, col_index=None):
        # adds css_class to every cell
        if col_index is None:
            gen = ((row, cell) for row in self.body.childs for cell in row.childs)
            for (row, cell) in gen:
                cell.attr(klass=css_class)
            return

        for row in self._iter_rows(col_index):
            row.childs[col_index].attr(klass=css_class)

    def row_class(self, css_class, row_index=None):
        # adds css_class to every row
        if row_index is None:
            for row in self.body.childs:
                row.attr(klass=css_class)
        elif self.is_row_within_bounds(row_index):
            self.body.childs[row_index].attr(klass=css_class)

    def map_col(self, col_function, col_index=None, ignore_errors=True):
        # applies function to every cell
        if col_index is None:
            self.map_table(col_function)
            return self

        try:
            for row in self._iter_rows(col_index):
                row.childs[col_index].apply_function(col_function)
        except Exception as ex:
            if ignore_errors:
                pass
            else:
                raise ex

    def map_row(self, row_function, row_index=None, ignore_errors=True):
        # applies function to every row
        if row_index is None:
            self.map_table(row_function)
            return self

        if self.is_row_within_bounds(row_index):
            gen = (
                cell
                for cell in self.body.childs[row_index].childs
                if len(cell.childs) > 0
            )
            self.apply_function_to_cells(gen, row_function, ignore_errors)

    def map_table(self, format_function, ignore_errors=True):
        for row in self.body.childs:
            gen = (cell for cell in row.childs if len(cell.childs) > 0)
            self.apply_function_to_cells(gen, format_function, ignore_errors)

    @staticmethod
    def apply_function_to_cells(gen, format_function, ignore_errors):
        try:
            for cell in gen:
                cell.apply_function(format_function)
        except Exception as ex:
            if ignore_errors:
                pass
            else:
                raise ex

    def make_scope(self, col_scope_list=None, row_scope_list=None):
        """Makes scopes and converts Td to Th for given arguments
        which represent lists of tuples (row_index, col_index)"""
        for scope, itm in ((col_scope_list, "col"), (row_scope_list, "row")):
            if scope is not None and len(scope) > 0:
                self.apply_scope(scope, itm)

    def apply_scope(self, scope_list, scope_tag):
        gen = (
            (row_index, col_index)
            for row_index, col_index in scope_list
            if self.is_row_within_bounds(row_index)
            and self.is_col_within_bounds(col_index, self.body.childs[row_index])
            and len(self.body.childs[row_index].childs[col_index].childs) > 0
        )

        for row_index, col_index in gen:
            cell = self.body.childs[row_index].childs[col_index]
            self.body.childs[row_index].childs[col_index] = tags.Th()(cell.childs[0])
            self.body.childs[row_index].childs[col_index].attrs = copy(cell.attrs)
            self.body.childs[row_index].childs[col_index].attr(scope=scope_tag)

    def is_row_within_bounds(self, row_index):
        if row_index >= 0 and (row_index < len(self.body.childs)):
            return True
        raise WidgetDataError(self, "Row index should be within table bounds")

    def is_col_within_bounds(self, col_index, row):
        if col_index >= 0 and (col_index < len(row.childs)):
            return True
        raise WidgetDataError(self, "Column index should be within table bounds")