summaryrefslogtreecommitdiffstats
path: root/terminaltables/base_table.py
blob: 281d5a3d35c2e0c66873c4f44697cf140b26af46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""Base table class. Define just the bare minimum to build tables."""

from terminaltables.build import build_border, build_row, flatten
from terminaltables.width_and_alignment import align_and_pad_cell, max_dimensions


class BaseTable(object):
    """Base table class.

    :ivar iter table_data: List (empty or list of lists of strings) representing the table.
    :ivar str title: Optional title to show within the top border of the table.
    :ivar bool inner_column_border: Separates columns.
    :ivar bool inner_footing_row_border: Show a border before the last row.
    :ivar bool inner_heading_row_border: Show a border after the first row.
    :ivar bool inner_row_border: Show a border in between every row.
    :ivar bool outer_border: Show the top, left, right, and bottom border.
    :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
    :ivar int padding_left: Number of spaces to pad on the left side of every cell.
    :ivar int padding_right: Number of spaces to pad on the right side of every cell.
    """

    CHAR_F_INNER_HORIZONTAL = '-'
    CHAR_F_INNER_INTERSECT = '+'
    CHAR_F_INNER_VERTICAL = '|'
    CHAR_F_OUTER_LEFT_INTERSECT = '+'
    CHAR_F_OUTER_LEFT_VERTICAL = '|'
    CHAR_F_OUTER_RIGHT_INTERSECT = '+'
    CHAR_F_OUTER_RIGHT_VERTICAL = '|'
    CHAR_H_INNER_HORIZONTAL = '-'
    CHAR_H_INNER_INTERSECT = '+'
    CHAR_H_INNER_VERTICAL = '|'
    CHAR_H_OUTER_LEFT_INTERSECT = '+'
    CHAR_H_OUTER_LEFT_VERTICAL = '|'
    CHAR_H_OUTER_RIGHT_INTERSECT = '+'
    CHAR_H_OUTER_RIGHT_VERTICAL = '|'
    CHAR_INNER_HORIZONTAL = '-'
    CHAR_INNER_INTERSECT = '+'
    CHAR_INNER_VERTICAL = '|'
    CHAR_OUTER_BOTTOM_HORIZONTAL = '-'
    CHAR_OUTER_BOTTOM_INTERSECT = '+'
    CHAR_OUTER_BOTTOM_LEFT = '+'
    CHAR_OUTER_BOTTOM_RIGHT = '+'
    CHAR_OUTER_LEFT_INTERSECT = '+'
    CHAR_OUTER_LEFT_VERTICAL = '|'
    CHAR_OUTER_RIGHT_INTERSECT = '+'
    CHAR_OUTER_RIGHT_VERTICAL = '|'
    CHAR_OUTER_TOP_HORIZONTAL = '-'
    CHAR_OUTER_TOP_INTERSECT = '+'
    CHAR_OUTER_TOP_LEFT = '+'
    CHAR_OUTER_TOP_RIGHT = '+'

    def __init__(self, table_data, title=None):
        """Constructor.

        :param iter table_data: List (empty or list of lists of strings) representing the table.
        :param title: Optional title to show within the top border of the table.
        """
        self.table_data = table_data
        self.title = title

        self.inner_column_border = True
        self.inner_footing_row_border = False
        self.inner_heading_row_border = True
        self.inner_row_border = False
        self.outer_border = True

        self.justify_columns = dict()  # {0: 'right', 1: 'left', 2: 'center'}
        self.padding_left = 1
        self.padding_right = 1

    def horizontal_border(self, style, outer_widths):
        """Build any kind of horizontal border for the table.

        :param str style: Type of border to return.
        :param iter outer_widths: List of widths (with padding) for each column.

        :return: Prepared border as a tuple of strings.
        :rtype: tuple
        """
        if style == 'top':
            horizontal = self.CHAR_OUTER_TOP_HORIZONTAL
            left = self.CHAR_OUTER_TOP_LEFT
            intersect = self.CHAR_OUTER_TOP_INTERSECT if self.inner_column_border else ''
            right = self.CHAR_OUTER_TOP_RIGHT
            title = self.title
        elif style == 'bottom':
            horizontal = self.CHAR_OUTER_BOTTOM_HORIZONTAL
            left = self.CHAR_OUTER_BOTTOM_LEFT
            intersect = self.CHAR_OUTER_BOTTOM_INTERSECT if self.inner_column_border else ''
            right = self.CHAR_OUTER_BOTTOM_RIGHT
            title = None
        elif style == 'heading':
            horizontal = self.CHAR_H_INNER_HORIZONTAL
            left = self.CHAR_H_OUTER_LEFT_INTERSECT if self.outer_border else ''
            intersect = self.CHAR_H_INNER_INTERSECT if self.inner_column_border else ''
            right = self.CHAR_H_OUTER_RIGHT_INTERSECT if self.outer_border else ''
            title = None
        elif style == 'footing':
            horizontal = self.CHAR_F_INNER_HORIZONTAL
            left = self.CHAR_F_OUTER_LEFT_INTERSECT if self.outer_border else ''
            intersect = self.CHAR_F_INNER_INTERSECT if self.inner_column_border else ''
            right = self.CHAR_F_OUTER_RIGHT_INTERSECT if self.outer_border else ''
            title = None
        else:
            horizontal = self.CHAR_INNER_HORIZONTAL
            left = self.CHAR_OUTER_LEFT_INTERSECT if self.outer_border else ''
            intersect = self.CHAR_INNER_INTERSECT if self.inner_column_border else ''
            right = self.CHAR_OUTER_RIGHT_INTERSECT if self.outer_border else ''
            title = None
        return build_border(outer_widths, horizontal, left, intersect, right, title)

    def gen_row_lines(self, row, style, inner_widths, height):
        r"""Combine cells in row and group them into lines with vertical borders.

        Caller is expected to pass yielded lines to ''.join() to combine them into a printable line. Caller must append
        newline character to the end of joined line.

        In:
        ['Row One Column One', 'Two', 'Three']
        Out:
        [
            ('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
        ]

        In:
        ['Row One\nColumn One', 'Two', 'Three'],
        Out:
        [
            ('|', ' Row One    ', '|', ' Two ', '|', ' Three ', '|'),
            ('|', ' Column One ', '|', '     ', '|', '       ', '|'),
        ]

        :param iter row: One row in the table. List of cells.
        :param str style: Type of border characters to use.
        :param iter inner_widths: List of widths (no padding) for each column.
        :param int height: Inner height (no padding) (number of lines) to expand row to.

        :return: Yields lines split into components in a list. Caller must ''.join() line.
        """
        cells_in_row = list()

        # Resize row if it doesn't have enough cells.
        if len(row) != len(inner_widths):
            row = row + [''] * (len(inner_widths) - len(row))

        # Pad and align each cell. Split each cell into lines to support multi-line cells.
        for i, cell in enumerate(row):
            align = (self.justify_columns.get(i),)
            inner_dimensions = (inner_widths[i], height)
            padding = (self.padding_left, self.padding_right, 0, 0)
            cells_in_row.append(align_and_pad_cell(cell, align, inner_dimensions, padding))

        # Determine border characters.
        if style == 'heading':
            left = self.CHAR_H_OUTER_LEFT_VERTICAL if self.outer_border else ''
            center = self.CHAR_H_INNER_VERTICAL if self.inner_column_border else ''
            right = self.CHAR_H_OUTER_RIGHT_VERTICAL if self.outer_border else ''
        elif style == 'footing':
            left = self.CHAR_F_OUTER_LEFT_VERTICAL if self.outer_border else ''
            center = self.CHAR_F_INNER_VERTICAL if self.inner_column_border else ''
            right = self.CHAR_F_OUTER_RIGHT_VERTICAL if self.outer_border else ''
        else:
            left = self.CHAR_OUTER_LEFT_VERTICAL if self.outer_border else ''
            center = self.CHAR_INNER_VERTICAL if self.inner_column_border else ''
            right = self.CHAR_OUTER_RIGHT_VERTICAL if self.outer_border else ''

        # Yield each line.
        for line in build_row(cells_in_row, left, center, right):
            yield line

    def gen_table(self, inner_widths, inner_heights, outer_widths):
        """Combine everything and yield every line of the entire table with borders.

        :param iter inner_widths: List of widths (no padding) for each column.
        :param iter inner_heights: List of heights (no padding) for each row.
        :param iter outer_widths: List of widths (with padding) for each column.
        :return:
        """
        # Yield top border.
        if self.outer_border:
            yield self.horizontal_border('top', outer_widths)

        # Yield table body.
        row_count = len(self.table_data)
        last_row_index, before_last_row_index = row_count - 1, row_count - 2
        for i, row in enumerate(self.table_data):
            # Yield the row line by line (e.g. multi-line rows).
            if self.inner_heading_row_border and i == 0:
                style = 'heading'
            elif self.inner_footing_row_border and i == last_row_index:
                style = 'footing'
            else:
                style = 'row'
            for line in self.gen_row_lines(row, style, inner_widths, inner_heights[i]):
                yield line
            # If this is the last row then break. No separator needed.
            if i == last_row_index:
                break
            # Yield heading separator.
            if self.inner_heading_row_border and i == 0:
                yield self.horizontal_border('heading', outer_widths)
            # Yield footing separator.
            elif self.inner_footing_row_border and i == before_last_row_index:
                yield self.horizontal_border('footing', outer_widths)
            # Yield row separator.
            elif self.inner_row_border:
                yield self.horizontal_border('row', outer_widths)

        # Yield bottom border.
        if self.outer_border:
            yield self.horizontal_border('bottom', outer_widths)

    @property
    def table(self):
        """Return a large string of the entire table ready to be printed to the terminal."""
        dimensions = max_dimensions(self.table_data, self.padding_left, self.padding_right)[:3]
        return flatten(self.gen_table(*dimensions))