@@ -98,6 +98,10 @@ def table(
98
98
min_width : Optional [int ] = None ,
99
99
row_dividers : bool = True ,
100
100
header : bool = True ,
101
+ hframe_char : str = "-" ,
102
+ vframe_char : str = "|" ,
103
+ corner_char : str = "+" ,
104
+ header_char : str = "=" ,
101
105
) -> str :
102
106
"""Encode the given columns as an ascii table
103
107
@@ -112,17 +116,32 @@ def table(
112
116
computed width based on content
113
117
row_dividers (bool): Include dividers between rows
114
118
header (bool): Include a special divider between header and rows
119
+ hframe_char (str): Single character for horizontal frame lines
120
+ vframe_char (str): Single character for vertical frame lines
121
+ corner_char (str): Single character for corners
122
+ header_char (str): Single character for the header horizontal divider
115
123
116
124
Returns:
117
125
table (str): The formatted table string
118
126
"""
127
+ # Validate arguments
128
+ if any (
129
+ len (char ) != 1 for char in [hframe_char , vframe_char , corner_char , header_char ]
130
+ ):
131
+ raise ValueError ("*_char args must be a single character" )
132
+ if len (set ([len (col ) for col in columns ])) > 1 :
133
+ raise ValueError ("All columns must have equal length" )
134
+
119
135
if max_width is None :
120
136
max_width = shutil .get_terminal_size ().columns
121
137
if min_width is None :
122
138
min_width = 2 * len (columns ) + 1 if width is None else width
123
139
124
140
# Stringify all column content
125
141
columns = [[str (val ) for val in col ] for col in columns ]
142
+ empty_cols = [i for i , col in enumerate (columns ) if not any (col )]
143
+ if empty_cols :
144
+ raise ValueError (f"Found empty column(s) when stringified: { empty_cols } " )
126
145
127
146
# Determine the raw max width of each column
128
147
max_col_width = max_width - 3 - 2 * (len (columns ) - 1 )
@@ -138,55 +157,50 @@ def table(
138
157
# For each column, determine the width as a percentage of the total width
139
158
pcts = [float (w ) / float (total_width ) for w in widths ]
140
159
col_widths = [int (p * usable_table_width ) + 3 for p in pcts ]
141
- col_widths [- 1 ] = usable_table_width - sum (col_widths [:- 1 ])
142
-
143
- # Adjust if possible to compensate for collapsed columns
144
- collapsed = [(i , w ) for i , w in enumerate (col_widths ) if w - 2 < 2 ]
145
- extra = sorted (
146
- [(w , i ) for i , w in enumerate (col_widths ) if (i , w ) not in collapsed ],
147
- key = lambda x : x [0 ],
148
- )
149
- for (i , w ) in collapsed :
150
- assert len (extra ) > 0 and extra [0 ][0 ] - w > 2 , "No extra to borrow from"
151
- padding = 2 - w
152
- col_widths [extra [0 ][1 ]] = extra [0 ][0 ] - padding
153
- col_widths [i ] = w + padding
154
- extra = sorted (extra , key = lambda x : x [0 ])
160
+ if col_widths :
161
+ col_widths [- 1 ] = usable_table_width - sum (col_widths [:- 1 ])
162
+ else :
163
+ col_widths = [0 ]
155
164
156
165
# Prepare the rows
157
166
wrapped_cols = []
158
167
for i , col in enumerate (columns ):
159
168
wrapped_cols .append ([])
160
169
for entry in col :
161
- assert col_widths [i ] - 2 > 1 , f"Column width collapsed for col { i } "
170
+ if col_widths [i ] - 2 <= 1 :
171
+ raise ValueError (f"Column width collapsed for col { i } " )
162
172
wrapped , _ = _word_wrap_to_len (entry , col_widths [i ] - 2 )
163
173
wrapped_cols [- 1 ].append (wrapped )
164
174
165
175
# Go row-by-row and add to the output
166
- out = _make_hline (table_width )
167
- n_rows = max ([len (col ) for col in columns ])
176
+ out = _make_hline (table_width , char = hframe_char , edge = corner_char )
177
+ n_rows = max ([len (col ) for col in columns ]) if columns else 0
178
+ if not n_rows :
179
+ if header :
180
+ out += _make_hline (table_width , char = header_char , edge = vframe_char )
181
+ out += _make_hline (table_width , char = hframe_char , edge = corner_char )
168
182
for r in range (n_rows ):
169
183
entries = [col [r ] if r < len (col ) else ["" ] for col in wrapped_cols ]
170
184
most_sublines = max ([len (e ) for e in entries ])
171
185
for i in range (most_sublines ):
172
186
line = ""
173
187
for c , entry in enumerate (entries ):
174
188
val = entry [i ] if len (entry ) > i else ""
175
- line += "| {}{}" .format (
176
- val , " " * (col_widths [c ] - _printed_len (val ) - 2 )
189
+ line += "{} {}{}" .format (
190
+ vframe_char , val , " " * (col_widths [c ] - _printed_len (val ) - 2 )
177
191
)
178
- line += "| \n "
192
+ line += f" { vframe_char } \n "
179
193
out += line
180
194
if r == 0 :
181
195
if header :
182
- out += _make_hline (table_width , char = "=" , edge = "|" )
196
+ out += _make_hline (table_width , char = header_char , edge = vframe_char )
183
197
elif row_dividers :
184
- out += _make_hline (table_width , edge = "|" )
198
+ out += _make_hline (table_width , char = hframe_char , edge = vframe_char )
185
199
elif r < n_rows - 1 :
186
200
if row_dividers :
187
- out += _make_hline (table_width , edge = "|" )
201
+ out += _make_hline (table_width , char = hframe_char , edge = vframe_char )
188
202
else :
189
- out += _make_hline (table_width )
203
+ out += _make_hline (table_width , char = hframe_char , edge = corner_char )
190
204
191
205
return out
192
206
@@ -211,8 +225,8 @@ def _word_wrap_to_len(line: str, max_len: int) -> Tuple[List[str], int]:
211
225
sublines (List[str]): The lines wrapped to the target length
212
226
longest (int): The length of the longest wrapped line (<= max_len)
213
227
"""
214
- if _printed_len (line ) <= max_len :
215
- return [line ], _printed_len ( line )
228
+ if ( printed_len := _printed_len (line ) ) <= max_len :
229
+ return [line ], printed_len
216
230
else :
217
231
longest = 0
218
232
sublines = []
@@ -231,12 +245,14 @@ def _word_wrap_to_len(line: str, max_len: int) -> Tuple[List[str], int]:
231
245
subline += words [0 ][:cutoff ] + "- "
232
246
words [0 ] = words [0 ][cutoff :]
233
247
else :
234
- break
248
+ # NOTE: This _is_ covered in tests, but the coverage engine
249
+ # doesn't pick it up for some reason!
250
+ break # pragma: no cover
235
251
subline = subline [:- 1 ]
236
252
longest = max (longest , _printed_len (subline ))
237
253
sublines .append (subline )
238
254
return sublines , longest
239
255
240
256
241
- def _make_hline (table_width : int , char : str = "-" , edge : str = "+" ) -> str :
257
+ def _make_hline (table_width : int , char : str , edge : str ) -> str :
242
258
return "{}{}{}\n " .format (edge , char * (table_width - 2 ), edge )
0 commit comments