4
4
from typing import List , Optional , Union
5
5
6
6
7
+ class Alignment (enum .Enum ):
8
+ """Enum for alignment types"""
9
+
10
+ LEFT = 0
11
+ CENTER = 1
12
+ RIGHT = 2
13
+
14
+
7
15
@dataclass
8
16
class Options :
9
17
"""Class for storing options that the user sets"""
10
18
19
+ header : Optional [List ] = None
20
+ body : Optional [List [List ]] = None
21
+ footer : Optional [List ] = None
11
22
first_col_heading : bool = False
12
23
last_col_heading : bool = False
13
-
14
-
15
- class Alignment (enum .Enum ):
16
- """Enum for alignment types"""
17
-
18
- LEFT = 0
19
- RIGHT = 1
20
- CENTER = 2
24
+ column_widths : Optional [List [int ]] = None
25
+ alignments : Optional [List [Alignment ]] = None
21
26
22
27
23
28
class TableToAscii :
24
29
"""Class used to convert a 2D Python table to ASCII text"""
25
30
26
- def __init__ (
27
- self ,
28
- header : Optional [List ],
29
- body : Optional [List [List ]],
30
- footer : Optional [List ],
31
- options : Options ,
32
- ):
31
+ def __init__ (self , options : Options ):
33
32
"""Validate arguments and initialize fields"""
34
- # check if columns in header are different from footer
35
- if header and footer and len (header ) != len (footer ):
36
- raise ValueError ("Header row and footer row must have the same length" )
37
- # check if columns in header are different from body
38
- if header and body and len (body ) > 0 and len (header ) != len (body [0 ]):
39
- raise ValueError ("Header row and body rows must have the same length" )
40
- # check if columns in header are different from body
41
- if footer and body and len (body ) > 0 and len (footer ) != len (body [0 ]):
42
- raise ValueError ("Footer row and body rows must have the same length" )
43
- # check if any rows in body have a different number of columns
44
- if body and len (body ) and tuple (filter (lambda r : len (r ) != len (body [0 ]), body )):
45
- raise ValueError ("All rows in body must have the same length" )
46
-
47
33
# initialize fields
48
- self .__header = header
49
- self .__body = body
50
- self .__footer = footer
51
- self .__options = options
34
+ self .__header = options .header
35
+ self .__body = options .body
36
+ self .__footer = options .footer
37
+ self .__first_col_heading = options .first_col_heading
38
+ self .__last_col_heading = options .last_col_heading
39
+
40
+ # calculate number of columns
52
41
self .__columns = self .__count_columns ()
53
- self .__cell_widths = self .__get_column_widths ()
42
+
43
+ # check if footer has a different number of columns
44
+ if options .footer and len (options .footer ) != self .__columns :
45
+ raise ValueError (
46
+ "Footer must have the same number of columns as the other rows"
47
+ )
48
+ # check if any rows in body have a different number of columns
49
+ if options .body and any (len (row ) != self .__columns for row in options .body ):
50
+ raise ValueError (
51
+ "All rows in body must have the same number of columns as the other rows"
52
+ )
53
+
54
+ # calculate or use given column widths
55
+ self .__column_widths = options .column_widths or self .__auto_column_widths ()
56
+
57
+ # check if column widths specified have a different number of columns
58
+ if options .column_widths and len (options .column_widths ) != self .__columns :
59
+ raise ValueError (
60
+ "Length of `column_widths` list must equal the number of columns"
61
+ )
62
+ # check if column widths are not all at least 2
63
+ if options .column_widths and min (options .column_widths ) < 2 :
64
+ raise ValueError (
65
+ "All values in `column_widths` must be greater than or equal to 2"
66
+ )
67
+
68
+ self .__alignments = options .alignments or [Alignment .CENTER ] * self .__columns
69
+
70
+ # check if alignments specified have a different number of columns
71
+ if options .alignments and len (options .alignments ) != self .__columns :
72
+ raise ValueError (
73
+ "Length of `alignments` list must equal the number of columns"
74
+ )
54
75
55
76
"""
56
77
╔═════╦═══════════════════════╗ ABBBBBCBBBBBDBBBBBDBBBBBDBBBBBE
@@ -99,11 +120,11 @@ def __count_columns(self) -> int:
99
120
return len (self .__body [0 ])
100
121
return 0
101
122
102
- def __get_column_widths (self ) -> List [int ]:
123
+ def __auto_column_widths (self ) -> List [int ]:
103
124
"""Get the minimum number of characters needed for the values
104
125
in each column in the table with 1 space of padding on each side.
105
126
"""
106
- col_counts = []
127
+ column_widths = []
107
128
for i in range (self .__columns ):
108
129
# number of characters in column of i of header, each body row, and footer
109
130
header_size = len (self .__header [i ]) if self .__header else 0
@@ -112,10 +133,10 @@ def __get_column_widths(self) -> List[int]:
112
133
)
113
134
footer_size = len (self .__footer [i ]) if self .__footer else 0
114
135
# get the max and add 2 for padding each side with a space
115
- col_counts .append (max (header_size , * body_size , footer_size ) + 2 )
116
- return col_counts
136
+ column_widths .append (max (header_size , * body_size , footer_size ) + 2 )
137
+ return column_widths
117
138
118
- def __pad (self , text : str , width : int , alignment : Alignment = Alignment . CENTER ):
139
+ def __pad (self , text : str , width : int , alignment : Alignment ):
119
140
"""Pad a string of text to a given width with specified alignment"""
120
141
if alignment == Alignment .LEFT :
121
142
# pad with spaces on the end
@@ -139,24 +160,27 @@ def __row_to_ascii(
139
160
filler : Union [str , List ],
140
161
) -> str :
141
162
"""Assembles a row of the ascii table"""
142
- first_heading = self .__options .first_col_heading
143
- last_heading = self .__options .last_col_heading
144
163
# left edge of the row
145
164
output = left_edge
146
165
# add columns
147
166
for i in range (self .__columns ):
148
167
# content between separators
149
168
output += (
150
169
# edge or row separator if filler is a specific character
151
- filler * self .__cell_widths [i ]
170
+ filler * self .__column_widths [i ]
152
171
if isinstance (filler , str )
153
172
# otherwise, use the column content
154
- else self .__pad (str (filler [i ]), self .__cell_widths [i ])
173
+ else self .__pad (
174
+ str (filler [i ]), self .__column_widths [i ], self .__alignments [i ]
175
+ )
155
176
)
156
177
# column seperator
157
178
sep = column_seperator
158
- if (i == 0 and first_heading ) or (i == self .__columns - 2 and last_heading ):
159
- # use column heading if option is specified
179
+ if i == 0 and self .__first_col_heading :
180
+ # use column heading if first column option is specified
181
+ sep = heading_col_sep
182
+ elif i == self .__columns - 2 and self .__last_col_heading :
183
+ # use column heading if last column option is specified
160
184
sep = heading_col_sep
161
185
elif i == self .__columns - 1 :
162
186
# replace last seperator with symbol for edge of the row
@@ -225,16 +249,16 @@ def __footer_sep_to_ascii(self) -> str:
225
249
)
226
250
227
251
def __body_to_ascii (self ) -> str :
228
- output : str = ""
229
- for row in self .__body :
230
- output += self .__row_to_ascii (
252
+ return "" .join (
253
+ self .__row_to_ascii (
231
254
left_edge = self .__parts ["left_and_right_edge" ],
232
255
heading_col_sep = self .__parts ["heading_col_sep" ],
233
256
column_seperator = self .__parts ["middle_edge" ],
234
257
right_edge = self .__parts ["left_and_right_edge" ],
235
258
filler = row ,
236
259
)
237
- return output
260
+ for row in self .__body
261
+ )
238
262
239
263
def to_ascii (self ) -> str :
240
264
# top row of table
@@ -256,17 +280,16 @@ def to_ascii(self) -> str:
256
280
return table
257
281
258
282
259
- def table2ascii (
260
- header : Optional [List ] = None ,
261
- body : Optional [List [List ]] = None ,
262
- footer : Optional [List ] = None ,
263
- ** options ,
264
- ) -> str :
283
+ def table2ascii (** options ) -> str :
265
284
"""Convert a 2D Python table to ASCII text
266
285
267
286
### Arguments
268
287
:param header: :class:`Optional[List]` List of column values in the table's header row
269
288
:param body: :class:`Optional[List[List]]` 2-dimensional list of values in the table's body
270
289
:param footer: :class:`Optional[List]` List of column values in the table's footer row
290
+ :param column_widths: :class:`Optional[List[int]]` List of widths in characters for each column (defaults to auto-sizing)
291
+ :param alignments: :class:`Optional[List[Alignment]]` List of alignments (ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`)
292
+ :param first_col_heading: :class:`Optional[bool]` Whether to add a header column separator after the first column
293
+ :param last_col_heading: :class:`Optional[bool]` Whether to add a header column separator before the last column
271
294
"""
272
- return TableToAscii (header , body , footer , Options (** options )).to_ascii ()
295
+ return TableToAscii (Options (** options )).to_ascii ()
0 commit comments