44from typing import List , Optional , Union
55
66
7+ class Alignment (enum .Enum ):
8+ """Enum for alignment types"""
9+
10+ LEFT = 0
11+ CENTER = 1
12+ RIGHT = 2
13+
14+
715@dataclass
816class Options :
917 """Class for storing options that the user sets"""
1018
19+ header : Optional [List ] = None
20+ body : Optional [List [List ]] = None
21+ footer : Optional [List ] = None
1122 first_col_heading : bool = False
1223 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
2126
2227
2328class TableToAscii :
2429 """Class used to convert a 2D Python table to ASCII text"""
2530
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 ):
3332 """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-
4733 # 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
5241 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+ )
5475
5576 """
5677 ╔═════╦═══════════════════════╗ ABBBBBCBBBBBDBBBBBDBBBBBDBBBBBE
@@ -99,11 +120,11 @@ def __count_columns(self) -> int:
99120 return len (self .__body [0 ])
100121 return 0
101122
102- def __get_column_widths (self ) -> List [int ]:
123+ def __auto_column_widths (self ) -> List [int ]:
103124 """Get the minimum number of characters needed for the values
104125 in each column in the table with 1 space of padding on each side.
105126 """
106- col_counts = []
127+ column_widths = []
107128 for i in range (self .__columns ):
108129 # number of characters in column of i of header, each body row, and footer
109130 header_size = len (self .__header [i ]) if self .__header else 0
@@ -112,10 +133,10 @@ def __get_column_widths(self) -> List[int]:
112133 )
113134 footer_size = len (self .__footer [i ]) if self .__footer else 0
114135 # 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
117138
118- def __pad (self , text : str , width : int , alignment : Alignment = Alignment . CENTER ):
139+ def __pad (self , text : str , width : int , alignment : Alignment ):
119140 """Pad a string of text to a given width with specified alignment"""
120141 if alignment == Alignment .LEFT :
121142 # pad with spaces on the end
@@ -139,24 +160,27 @@ def __row_to_ascii(
139160 filler : Union [str , List ],
140161 ) -> str :
141162 """Assembles a row of the ascii table"""
142- first_heading = self .__options .first_col_heading
143- last_heading = self .__options .last_col_heading
144163 # left edge of the row
145164 output = left_edge
146165 # add columns
147166 for i in range (self .__columns ):
148167 # content between separators
149168 output += (
150169 # edge or row separator if filler is a specific character
151- filler * self .__cell_widths [i ]
170+ filler * self .__column_widths [i ]
152171 if isinstance (filler , str )
153172 # 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+ )
155176 )
156177 # column seperator
157178 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
160184 sep = heading_col_sep
161185 elif i == self .__columns - 1 :
162186 # replace last seperator with symbol for edge of the row
@@ -225,16 +249,16 @@ def __footer_sep_to_ascii(self) -> str:
225249 )
226250
227251 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 (
231254 left_edge = self .__parts ["left_and_right_edge" ],
232255 heading_col_sep = self .__parts ["heading_col_sep" ],
233256 column_seperator = self .__parts ["middle_edge" ],
234257 right_edge = self .__parts ["left_and_right_edge" ],
235258 filler = row ,
236259 )
237- return output
260+ for row in self .__body
261+ )
238262
239263 def to_ascii (self ) -> str :
240264 # top row of table
@@ -256,17 +280,16 @@ def to_ascii(self) -> str:
256280 return table
257281
258282
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 :
265284 """Convert a 2D Python table to ASCII text
266285
267286 ### Arguments
268287 :param header: :class:`Optional[List]` List of column values in the table's header row
269288 :param body: :class:`Optional[List[List]]` 2-dimensional list of values in the table's body
270289 :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
271294 """
272- return TableToAscii (header , body , footer , Options (** options )).to_ascii ()
295+ return TableToAscii (Options (** options )).to_ascii ()
0 commit comments