blob: 504b04dd592cdd25e8837831cb2d0ad841d2bcd7 [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))
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300309
kdanylov aka koder150b2192017-04-01 16:53:01 +0300310 self._rows.append(cells)
311
312 def add_rows(self, rows, header=True):
313 """Add several rows in the rows stack
314
315 - The 'rows' argument can be either an iterator returning arrays,
316 or a by-dimensional array
317 - 'header' specifies if the first row should be used as the header
318 of the table
319 """
320
321 # nb: don't use 'iter' on by-dimensional arrays, to get a
322 # usable code for python 2.1
323 if header:
324 if hasattr(rows, '__iter__') and hasattr(rows, 'next'):
325 self.header(rows.next())
326 else:
327 self.header(rows[0])
328 rows = rows[1:]
329 for row in rows:
330 self.add_row(row)
331
332 def draw(self):
333 """Draw the table
334
335 - the table is returned as a whole string
336 """
337
338 if not self._header and not self._rows:
339 return
340 self._compute_cols_width()
341 self._check_align()
342 out = ""
343
344 if self._has_border():
345 out += self._hline(*self._chars_top)
346
347 if self._header:
348 out += self._draw_line(self._header, isheader=True)
349 if self._has_header():
350 out += self._hline(*self._chars_header)
351
352 length = 0
353 for row in self._rows:
354 length += 1
355 out += self._draw_line(row)
356 if self._has_hlines() and length < len(self._rows):
357 out += self._hline(*self._chars_middle)
358
359 if self._has_border():
360 out += self._hline(*self._chars_bottom)
361
362 return out[:-1]
363
364 def _str(self, i, x):
365 """Handles string formatting of cell data
366
367 i - index of the cell datatype in self._dtype
368 x - cell data to format
369 """
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300370 if isinstance(x, str):
371 return x
372
kdanylov aka koder150b2192017-04-01 16:53:01 +0300373 try:
374 f = float(x)
375 except:
376 return obj2unicode(x)
377
378 n = self._precision
379 dtype = self._dtype[i]
380
381 if dtype == 'i':
382 return str(int(round(f)))
383 elif dtype == 'f':
384 return '%.*f' % (n, f)
385 elif dtype == 'e':
386 return '%.*e' % (n, f)
387 elif dtype == 't':
388 return obj2unicode(x)
389 else:
390 if f - round(f) == 0:
391 if abs(f) > 1e8:
392 return '%.*e' % (n, f)
393 else:
394 return str(int(round(f)))
395 else:
396 if abs(f) > 1e8:
397 return '%.*e' % (n, f)
398 else:
399 return '%.*f' % (n, f)
400
401 def _check_row_size(self, array):
402 """Check that the specified array fits the previous rows size
403 """
404
405 if not self._row_size:
406 self._row_size = len(array)
407 elif self._row_size != len(array):
408 raise ArraySizeError("array should contain %d elements" \
409 % self._row_size)
410
411 def _has_vlines(self):
412 """Return a boolean, if vlines are required or not
413 """
414
415 return self._deco & Texttable.VLINES > 0
416
417 def _has_hlines(self):
418 """Return a boolean, if hlines are required or not
419 """
420
421 return self._deco & Texttable.HLINES > 0
422
423 def _has_border(self):
424 """Return a boolean, if border is required or not
425 """
426
427 return self._deco & Texttable.BORDER > 0
428
429 def _has_header(self):
430 """Return a boolean, if header line is required or not
431 """
432
433 return self._deco & Texttable.HEADER > 0
434
435 def _hline(self, left, horiz, cross, right):
436 """Return a string used to separated rows or separate header from rows"""
437
438 # compute cell separator
439 sep = horiz + (cross if self._has_vlines() else horiz) + horiz
440
441 # build the line
442 line = sep.join([horiz * n for n in self._width])
443
444 # add border if needed
445 if self._has_border():
446 line = left + horiz + line + horiz + right
447
448 return line + "\n"
449
450 def _len_cell(self, cell):
451 """Return the width of the cell
452
453 Special characters are taken into account to return the width of the
454 cell, such like newlines and tabs
455 """
456
457 cell_lines = cell.split('\n')
458 maxi = 0
459 for line in cell_lines:
460 length = 0
461 parts = line.split('\t')
462 for part, i in zip(parts, list(range(1, len(parts) + 1))):
463 length = length + len(part)
464 if i < len(parts):
465 length = (length//8 + 1) * 8
466 maxi = max(maxi, length)
467 return maxi
468
469 def _compute_cols_width(self):
470 """Return an array with the width of each column
471
472 If a specific width has been specified, exit. If the total of the
473 columns width exceed the table desired width, another width will be
474 computed to fit, and cells will be wrapped.
475 """
476
477 if hasattr(self, "_width"):
478 return
479 maxi = []
480 if self._header:
481 maxi = [ self._len_cell(x) for x in self._header ]
482 for row in self._rows:
483 for cell,i in zip(row, list(range(len(row)))):
484 try:
485 maxi[i] = max(maxi[i], self._len_cell(cell))
486 except (TypeError, IndexError):
487 maxi.append(self._len_cell(cell))
488 items = len(maxi)
489 length = sum(maxi)
490 if self._max_width and length + items * 3 + 1 > self._max_width:
491 maxi = [
492 int(round(self._max_width / (length + items * 3 + 1) * n))
493 for n in maxi
494 ]
495 self._width = maxi
496
497 def _check_align(self):
498 """Check if alignment has been specified, set default one if not
499 """
500
501 if not hasattr(self, "_align"):
502 self._align = ["l"] * self._row_size
503 if not hasattr(self, "_valign"):
504 self._valign = ["t"] * self._row_size
505
506 def _draw_line(self, line, isheader=False):
507 """Draw a line
508
509 Loop over a single cell length, over all the cells
510 """
511
512 line = self._splitit(line, isheader)
513 space = " "
514 out = ""
515 for i in range(len(line[0])):
516 if self._has_border():
517 out += "%s " % self._char_vert
518 length = 0
519 for cell, width, align in zip(line, self._width, self._align):
520 length += 1
521 cell_line = cell[i]
522 fill = width - len(cell_line)
523 if isheader:
524 align = "c"
525 if align == "r":
526 out += "%s " % (fill * space + cell_line)
527 elif align == "c":
528 out += "%s " % (int(fill/2) * space + cell_line + int(fill/2 + fill%2) * space)
529 else:
530 out += "%s " % (cell_line + fill * space)
531 if length < len(line):
532 out += "%s " % [space, self._char_vert][self._has_vlines()]
533 out += "%s\n" % ['', self._char_vert][self._has_border()]
534 return out
535
536 def _splitit(self, line, isheader):
537 """Split each element of line to fit the column width
538
539 Each element is turned into a list, result of the wrapping of the
540 string to the desired width
541 """
542
543 line_wrapped = []
544 for cell, width in zip(line, self._width):
545 array = []
546 for c in cell.split('\n'):
547 if c.strip() == "":
548 array.append("")
549 else:
550 array.extend(textwrap.wrap(c, width))
551 line_wrapped.append(array)
552 max_cell_lines = reduce(max, list(map(len, line_wrapped)))
553 for cell, valign in zip(line_wrapped, self._valign):
554 if isheader:
555 valign = "t"
556 if valign == "m":
557 missing = max_cell_lines - len(cell)
558 cell[:0] = [""] * int(missing / 2)
559 cell.extend([""] * int(missing / 2 + missing % 2))
560 elif valign == "b":
561 cell[:0] = [""] * (max_cell_lines - len(cell))
562 else:
563 cell.extend([""] * (max_cell_lines - len(cell)))
564 return line_wrapped
565
566
567if __name__ == '__main__':
568 table = Texttable()
569 table.set_cols_align(["l", "r", "c"])
570 table.set_cols_valign(["t", "m", "b"])
571 table.add_rows([["Name", "Age", "Nickname"],
572 ["Mr\nXavier\nHuon", 32, "Xav'"],
573 ["Mr\nBaptiste\nClement", 1, "Baby"],
574 ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"]])
575 print(table.draw() + "\n")
576
577 table = Texttable()
578 table.set_deco(Texttable.HEADER)
579 table.set_cols_dtype(['t', # text
580 'f', # float (decimal)
581 'e', # float (exponent)
582 'i', # integer
583 'a']) # automatic
584 table.set_cols_align(["l", "r", "r", "r", "l"])
585 table.add_rows([["text", "float", "exp", "int", "auto"],
586 ["abcd", "67", 654, 89, 128.001],
587 ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],
588 ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],
589 ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])
590 print(table.draw())