blob: 1917663c9f1a7eebe206c1da32c558031c56eddc [file] [log] [blame]
kdanylov aka koder150b2192017-04-01 16:53:01 +03001# texttable - module for creating simple ASCII tables
2# Copyright (C) 2003-2015 Gerome Fournier <jef(at)foutaise.org>
3#
4# This library is free software; you can redistribute it and/or
5# modify it under the terms of the GNU Lesser General Public
6# License as published by the Free Software Foundation; either
7# version 2.1 of the License, or (at your option) any later version.
8#
9# This library is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public
15# License along with this library; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
17
18"""module for creating simple ASCII tables
19
20
21Example:
22
23 table = Texttable()
24 table.set_cols_align(["l", "r", "c"])
25 table.set_cols_valign(["t", "m", "b"])
26 table.add_rows([["Name", "Age", "Nickname"],
27 ["Mr\\nXavier\\nHuon", 32, "Xav'"],
28 ["Mr\\nBaptiste\\nClement", 1, "Baby"],
29 ["Mme\\nLouise\\nBourgeau", 28, "Lou\\n\\nLoue"]])
30 print table.draw() + "\\n"
31
32 table = Texttable()
33 table.set_deco(Texttable.HEADER)
34 table.set_cols_dtype(['t', # text
35 'f', # float (decimal)
36 'e', # float (exponent)
37 'i', # integer
38 'a']) # automatic
39 table.set_cols_align(["l", "r", "r", "r", "l"])
40 table.add_rows([["text", "float", "exp", "int", "auto"],
41 ["abcd", "67", 654, 89, 128.001],
42 ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],
43 ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],
44 ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])
45 print table.draw()
46
47Result:
48
49 +----------+-----+----------+
50 | Name | Age | Nickname |
51 +==========+=====+==========+
52 | Mr | | |
53 | Xavier | 32 | |
54 | Huon | | Xav' |
55 +----------+-----+----------+
56 | Mr | | |
57 | Baptiste | 1 | |
58 | Clement | | Baby |
59 +----------+-----+----------+
60 | Mme | | Lou |
61 | Louise | 28 | |
62 | Bourgeau | | Loue |
63 +----------+-----+----------+
64
65 text float exp int auto
66 ===========================================
67 abcd 67.000 6.540e+02 89 128.001
68 efgh 67.543 6.540e-01 90 1.280e+22
69 ijkl 0.000 5.000e-78 89 0.000
70 mnop 0.023 5.000e+78 92 1.280e+22
71"""
72
73from __future__ import division
74
75__all__ = ["Texttable", "ArraySizeError"]
76
77__author__ = 'Gerome Fournier <jef(at)foutaise.org>'
78__license__ = 'LGPL'
79__version__ = '0.8.8'
80__credits__ = """\
81Jeff Kowalczyk:
82 - textwrap improved import
83 - comment concerning header output
84
85Anonymous:
86 - add_rows method, for adding rows in one go
87
88Sergey Simonenko:
89 - redefined len() function to deal with non-ASCII characters
90
91Roger Lew:
92 - columns datatype specifications
93
94Brian Peterson:
95 - better handling of unicode errors
96
97Frank Sachsenheim:
98 - add Python 2/3-compatibility
99
100Maximilian Hils:
101 - fix minor bug for Python 3 compatibility
102
103frinkelpi:
104 - preserve empty lines
105"""
106
107import sys
108import string
109import unicodedata
110
111try:
112 if sys.version >= '2.3':
113 import textwrap
114 elif sys.version >= '2.2':
115 from optparse import textwrap
116 else:
117 from optik import textwrap
118except ImportError:
119 sys.stderr.write("Can't import textwrap module!\n")
120 raise
121
122if sys.version >= '2.7':
123 from functools import reduce
124
125if sys.version >= '3.0':
126 unicode_type = str
127 bytes_type = bytes
128else:
129 unicode_type = unicode
130 bytes_type = str
131
132
133def obj2unicode(obj):
134 """Return a unicode representation of a python object
135 """
136 if isinstance(obj, unicode_type):
137 return obj
138 elif isinstance(obj, bytes_type):
139 try:
140 return unicode_type(obj, 'utf-8')
141 except UnicodeDecodeError as strerror:
142 sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (obj, strerror))
143 return unicode_type(obj, 'utf-8', 'replace')
144 else:
145 return unicode_type(obj)
146
147
148def len(iterable):
149 """Redefining len here so it will be able to work with non-ASCII characters
150 """
151 if isinstance(iterable, bytes_type) or isinstance(iterable, unicode_type):
152 unicode_data = obj2unicode(iterable)
153 if hasattr(unicodedata, 'east_asian_width'):
154 w = unicodedata.east_asian_width
155 return sum([w(c) in 'WF' and 2 or 1 for c in unicode_data])
156 else:
157 return unicode_data.__len__()
158 else:
159 return iterable.__len__()
160
161
162class ArraySizeError(Exception):
163 """Exception raised when specified rows don't fit the required size
164 """
165
166 def __init__(self, msg):
167 self.msg = msg
168 Exception.__init__(self, msg, '')
169
170 def __str__(self):
171 return self.msg
172
173
174class Texttable:
175
176 BORDER = 1
177 HEADER = 1 << 1
178 HLINES = 1 << 2
179 VLINES = 1 << 3
180
181 def __init__(self, max_width=80):
182 """Constructor
183
184 - max_width is an integer, specifying the maximum width of the table
185 - if set to 0, size is unlimited, therefore cells won't be wrapped
186 """
187
188 if max_width <= 0:
189 max_width = False
190 self._max_width = max_width
191 self._precision = 3
192
193 self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | Texttable.HEADER
194 self._reset()
195 ## left, horiz, cross, right
196 self._chars_top = (chr(0x250c), chr(0x2500), chr(0x252c), chr(0x2510))
197 # self.chars_header = (chr(0x255e), chr(0x2550), chr(0x256a), chr(0x2561))
198 self._chars_header = (chr(0x251d), chr(0x2501), chr(0x253f), chr(0x2525))
199 self._chars_middle = (chr(0x251c), chr(0x2500), chr(0x253c), chr(0x2524))
200 self._chars_bottom = (chr(0x2514), chr(0x2500), chr(0x2534), chr(0x2518))
201 self._char_vert = chr(0x2502)
202 self._align = None
203
204 def _reset(self):
205 """Reset the instance
206
207 - reset rows and header
208 """
209
210 self._row_size = None
211 self._header = []
212 self._rows = []
213
214 def set_cols_align(self, array):
215 """Set the desired columns alignment
216
217 - the elements of the array should be either "l", "c" or "r":
218
219 * "l": column flushed left
220 * "c": column centered
221 * "r": column flushed right
222 """
223
224 self._check_row_size(array)
225 self._align = array
226
227 def set_cols_valign(self, array):
228 """Set the desired columns vertical alignment
229
230 - the elements of the array should be either "t", "m" or "b":
231
232 * "t": column aligned on the top of the cell
233 * "m": column aligned on the middle of the cell
234 * "b": column aligned on the bottom of the cell
235 """
236
237 self._check_row_size(array)
238 self._valign = array
239
240 def set_cols_dtype(self, array):
241 """Set the desired columns datatype for the cols.
242
243 - the elements of the array should be either "a", "t", "f", "e" or "i":
244
245 * "a": automatic (try to use the most appropriate datatype)
246 * "t": treat as text
247 * "f": treat as float in decimal format
248 * "e": treat as float in exponential format
249 * "i": treat as int
250
251 - by default, automatic datatyping is used for each column
252 """
253
254 self._check_row_size(array)
255 self._dtype = array
256
257 def set_cols_width(self, array):
258 """Set the desired columns width
259
260 - the elements of the array should be integers, specifying the
261 width of each column. For example:
262
263 [10, 20, 5]
264 """
265
266 self._check_row_size(array)
267 try:
268 array = list(map(int, array))
269 if reduce(min, array) <= 0:
270 raise ValueError
271 except ValueError:
272 sys.stderr.write("Wrong argument in column width specification\n")
273 raise
274 self._width = array
275
276 def set_precision(self, width):
277 """Set the desired precision for float/exponential formats
278
279 - width must be an integer >= 0
280
281 - default value is set to 3
282 """
283
284 if not type(width) is int or width < 0:
285 raise ValueError('width must be an integer greater then 0')
286 self._precision = width
287
288 def header(self, array):
289 """Specify the header of the table
290 """
291
292 self._check_row_size(array)
293 self._header = list(map(obj2unicode, array))
294
295 def add_row(self, array):
296 """Add a row in the rows stack
297
298 - cells can contain newlines and tabs
299 """
300
301 self._check_row_size(array)
302
303 if not hasattr(self, "_dtype"):
304 self._dtype = ["a"] * self._row_size
305
306 cells = []
307 for i, x in enumerate(array):
308 cells.append(self._str(i, x))
309 self._rows.append(cells)
310
311 def add_rows(self, rows, header=True):
312 """Add several rows in the rows stack
313
314 - The 'rows' argument can be either an iterator returning arrays,
315 or a by-dimensional array
316 - 'header' specifies if the first row should be used as the header
317 of the table
318 """
319
320 # nb: don't use 'iter' on by-dimensional arrays, to get a
321 # usable code for python 2.1
322 if header:
323 if hasattr(rows, '__iter__') and hasattr(rows, 'next'):
324 self.header(rows.next())
325 else:
326 self.header(rows[0])
327 rows = rows[1:]
328 for row in rows:
329 self.add_row(row)
330
331 def draw(self):
332 """Draw the table
333
334 - the table is returned as a whole string
335 """
336
337 if not self._header and not self._rows:
338 return
339 self._compute_cols_width()
340 self._check_align()
341 out = ""
342
343 if self._has_border():
344 out += self._hline(*self._chars_top)
345
346 if self._header:
347 out += self._draw_line(self._header, isheader=True)
348 if self._has_header():
349 out += self._hline(*self._chars_header)
350
351 length = 0
352 for row in self._rows:
353 length += 1
354 out += self._draw_line(row)
355 if self._has_hlines() and length < len(self._rows):
356 out += self._hline(*self._chars_middle)
357
358 if self._has_border():
359 out += self._hline(*self._chars_bottom)
360
361 return out[:-1]
362
363 def _str(self, i, x):
364 """Handles string formatting of cell data
365
366 i - index of the cell datatype in self._dtype
367 x - cell data to format
368 """
369 try:
370 f = float(x)
371 except:
372 return obj2unicode(x)
373
374 n = self._precision
375 dtype = self._dtype[i]
376
377 if dtype == 'i':
378 return str(int(round(f)))
379 elif dtype == 'f':
380 return '%.*f' % (n, f)
381 elif dtype == 'e':
382 return '%.*e' % (n, f)
383 elif dtype == 't':
384 return obj2unicode(x)
385 else:
386 if f - round(f) == 0:
387 if abs(f) > 1e8:
388 return '%.*e' % (n, f)
389 else:
390 return str(int(round(f)))
391 else:
392 if abs(f) > 1e8:
393 return '%.*e' % (n, f)
394 else:
395 return '%.*f' % (n, f)
396
397 def _check_row_size(self, array):
398 """Check that the specified array fits the previous rows size
399 """
400
401 if not self._row_size:
402 self._row_size = len(array)
403 elif self._row_size != len(array):
404 raise ArraySizeError("array should contain %d elements" \
405 % self._row_size)
406
407 def _has_vlines(self):
408 """Return a boolean, if vlines are required or not
409 """
410
411 return self._deco & Texttable.VLINES > 0
412
413 def _has_hlines(self):
414 """Return a boolean, if hlines are required or not
415 """
416
417 return self._deco & Texttable.HLINES > 0
418
419 def _has_border(self):
420 """Return a boolean, if border is required or not
421 """
422
423 return self._deco & Texttable.BORDER > 0
424
425 def _has_header(self):
426 """Return a boolean, if header line is required or not
427 """
428
429 return self._deco & Texttable.HEADER > 0
430
431 def _hline(self, left, horiz, cross, right):
432 """Return a string used to separated rows or separate header from rows"""
433
434 # compute cell separator
435 sep = horiz + (cross if self._has_vlines() else horiz) + horiz
436
437 # build the line
438 line = sep.join([horiz * n for n in self._width])
439
440 # add border if needed
441 if self._has_border():
442 line = left + horiz + line + horiz + right
443
444 return line + "\n"
445
446 def _len_cell(self, cell):
447 """Return the width of the cell
448
449 Special characters are taken into account to return the width of the
450 cell, such like newlines and tabs
451 """
452
453 cell_lines = cell.split('\n')
454 maxi = 0
455 for line in cell_lines:
456 length = 0
457 parts = line.split('\t')
458 for part, i in zip(parts, list(range(1, len(parts) + 1))):
459 length = length + len(part)
460 if i < len(parts):
461 length = (length//8 + 1) * 8
462 maxi = max(maxi, length)
463 return maxi
464
465 def _compute_cols_width(self):
466 """Return an array with the width of each column
467
468 If a specific width has been specified, exit. If the total of the
469 columns width exceed the table desired width, another width will be
470 computed to fit, and cells will be wrapped.
471 """
472
473 if hasattr(self, "_width"):
474 return
475 maxi = []
476 if self._header:
477 maxi = [ self._len_cell(x) for x in self._header ]
478 for row in self._rows:
479 for cell,i in zip(row, list(range(len(row)))):
480 try:
481 maxi[i] = max(maxi[i], self._len_cell(cell))
482 except (TypeError, IndexError):
483 maxi.append(self._len_cell(cell))
484 items = len(maxi)
485 length = sum(maxi)
486 if self._max_width and length + items * 3 + 1 > self._max_width:
487 maxi = [
488 int(round(self._max_width / (length + items * 3 + 1) * n))
489 for n in maxi
490 ]
491 self._width = maxi
492
493 def _check_align(self):
494 """Check if alignment has been specified, set default one if not
495 """
496
497 if not hasattr(self, "_align"):
498 self._align = ["l"] * self._row_size
499 if not hasattr(self, "_valign"):
500 self._valign = ["t"] * self._row_size
501
502 def _draw_line(self, line, isheader=False):
503 """Draw a line
504
505 Loop over a single cell length, over all the cells
506 """
507
508 line = self._splitit(line, isheader)
509 space = " "
510 out = ""
511 for i in range(len(line[0])):
512 if self._has_border():
513 out += "%s " % self._char_vert
514 length = 0
515 for cell, width, align in zip(line, self._width, self._align):
516 length += 1
517 cell_line = cell[i]
518 fill = width - len(cell_line)
519 if isheader:
520 align = "c"
521 if align == "r":
522 out += "%s " % (fill * space + cell_line)
523 elif align == "c":
524 out += "%s " % (int(fill/2) * space + cell_line + int(fill/2 + fill%2) * space)
525 else:
526 out += "%s " % (cell_line + fill * space)
527 if length < len(line):
528 out += "%s " % [space, self._char_vert][self._has_vlines()]
529 out += "%s\n" % ['', self._char_vert][self._has_border()]
530 return out
531
532 def _splitit(self, line, isheader):
533 """Split each element of line to fit the column width
534
535 Each element is turned into a list, result of the wrapping of the
536 string to the desired width
537 """
538
539 line_wrapped = []
540 for cell, width in zip(line, self._width):
541 array = []
542 for c in cell.split('\n'):
543 if c.strip() == "":
544 array.append("")
545 else:
546 array.extend(textwrap.wrap(c, width))
547 line_wrapped.append(array)
548 max_cell_lines = reduce(max, list(map(len, line_wrapped)))
549 for cell, valign in zip(line_wrapped, self._valign):
550 if isheader:
551 valign = "t"
552 if valign == "m":
553 missing = max_cell_lines - len(cell)
554 cell[:0] = [""] * int(missing / 2)
555 cell.extend([""] * int(missing / 2 + missing % 2))
556 elif valign == "b":
557 cell[:0] = [""] * (max_cell_lines - len(cell))
558 else:
559 cell.extend([""] * (max_cell_lines - len(cell)))
560 return line_wrapped
561
562
563if __name__ == '__main__':
564 table = Texttable()
565 table.set_cols_align(["l", "r", "c"])
566 table.set_cols_valign(["t", "m", "b"])
567 table.add_rows([["Name", "Age", "Nickname"],
568 ["Mr\nXavier\nHuon", 32, "Xav'"],
569 ["Mr\nBaptiste\nClement", 1, "Baby"],
570 ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"]])
571 print(table.draw() + "\n")
572
573 table = Texttable()
574 table.set_deco(Texttable.HEADER)
575 table.set_cols_dtype(['t', # text
576 'f', # float (decimal)
577 'e', # float (exponent)
578 'i', # integer
579 'a']) # automatic
580 table.set_cols_align(["l", "r", "r", "r", "l"])
581 table.add_rows([["text", "float", "exp", "int", "auto"],
582 ["abcd", "67", 654, 89, 128.001],
583 ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],
584 ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],
585 ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])
586 print(table.draw())