codekingpro/portable-devtools
114k
1# texttable - module to create simple ASCII tables2# Copyright (C) 2003-2023 Gerome Fournier <jef(at)foutaise.org>3 4"""module to create simple ASCII tables5 6 7Example:8 9 table = Texttable()10 table.set_cols_align(["l", "r", "c"])11 table.set_cols_valign(["t", "m", "b"])12 table.add_rows([["Name", "Age", "Nickname"],13 ["Mr\\nXavier\\nHuon", 32, "Xav'"],14 ["Mr\\nBaptiste\\nClement", 1, "Baby"],15 ["Mme\\nLouise\\nBourgeau", 28, "Lou\\n\\nLoue"]])16 print(table.draw())17 print()18 19 table = Texttable()20 table.set_deco(Texttable.HEADER)21 table.set_cols_dtype(['t', # text22 'f', # float (decimal)23 'e', # float (exponent)24 'i', # integer25 'a']) # automatic26 table.set_cols_align(["l", "r", "r", "r", "l"])27 table.add_rows([["text", "float", "exp", "int", "auto"],28 ["abcd", "67", 654, 89, 128.001],29 ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],30 ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],31 ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])32 print(table.draw())33 34Result:35 36 +----------+-----+----------+37 | Name | Age | Nickname |38 +==========+=====+==========+39 | Mr | | |40 | Xavier | 32 | |41 | Huon | | Xav' |42 +----------+-----+----------+43 | Mr | | |44 | Baptiste | 1 | |45 | Clement | | Baby |46 +----------+-----+----------+47 | Mme | | Lou |48 | Louise | 28 | |49 | Bourgeau | | Loue |50 +----------+-----+----------+51 52 text float exp int auto53 ===========================================54 abcd 67.000 6.540e+02 89 128.00155 efgh 67.543 6.540e-01 90 1.280e+2256 ijkl 0.000 5.000e-78 89 0.00057 mnop 0.023 5.000e+78 92 1.280e+2258"""59 60from __future__ import division61 62__all__ = ["Texttable", "ArraySizeError"]63 64__author__ = 'Gerome Fournier <jef(at)foutaise.org>'65__license__ = 'MIT'66__version__ = '1.7.0'67__credits__ = """\68Jeff Kowalczyk:69 - textwrap improved import70 - comment concerning header output71 72Anonymous:73 - add_rows method, for adding rows in one go74 75Sergey Simonenko:76 - redefined len() function to deal with non-ASCII characters77 78Roger Lew:79 - columns datatype specifications80 81Brian Peterson:82 - better handling of unicode errors83 84Frank Sachsenheim:85 - add Python 2/3-compatibility86 87Maximilian Hils:88 - fix minor bug for Python 3 compatibility89 90frinkelpi:91 - preserve empty lines92"""93 94import sys95import unicodedata96 97# define a text wrapping function to wrap some text98# to a specific width:99# - use cjkwrap if available (better CJK support)100# - fallback to textwrap otherwise101try:102 import cjkwrap103 def textwrapper(txt, width):104 return cjkwrap.wrap(txt, width)105except ImportError:106 try:107 import textwrap108 def textwrapper(txt, width):109 return textwrap.wrap(txt, width)110 except ImportError:111 sys.stderr.write("Can't import textwrap module!\n")112 raise113 114# define a function to calculate the rendering width of a unicode character115# - use wcwidth if available116# - fallback to unicodedata information otherwise117try:118 import wcwidth119 def uchar_width(c):120 """Return the rendering width of a unicode character121 """122 return max(0, wcwidth.wcwidth(c))123except ImportError:124 def uchar_width(c):125 """Return the rendering width of a unicode character126 """127 if unicodedata.east_asian_width(c) in 'WF':128 return 2129 elif unicodedata.combining(c):130 return 0131 else:132 return 1133 134from functools import reduce135 136if sys.version_info >= (3, 0):137 unicode_type = str138 bytes_type = bytes139else:140 unicode_type = unicode141 bytes_type = str142 143 144def obj2unicode(obj):145 """Return a unicode representation of a python object146 """147 if isinstance(obj, unicode_type):148 return obj149 elif isinstance(obj, bytes_type):150 try:151 return unicode_type(obj, 'utf-8')152 except UnicodeDecodeError as strerror:153 sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (obj, strerror))154 return unicode_type(obj, 'utf-8', 'replace')155 else:156 return unicode_type(obj)157 158 159def len(iterable):160 """Redefining len here so it will be able to work with non-ASCII characters161 """162 if isinstance(iterable, bytes_type) or isinstance(iterable, unicode_type):163 return sum([uchar_width(c) for c in obj2unicode(iterable)])164 else:165 return iterable.__len__()166 167 168class ArraySizeError(Exception):169 """Exception raised when specified rows don't fit the required size170 """171 172 def __init__(self, msg):173 self.msg = msg174 Exception.__init__(self, msg, '')175 176 def __str__(self):177 return self.msg178 179 180class FallbackToText(Exception):181 """Used for failed conversion to float"""182 pass183 184 185class Texttable:186 187 BORDER = 1188 HEADER = 1 << 1189 HLINES = 1 << 2190 VLINES = 1 << 3191 192 def __init__(self, max_width=80):193 """Constructor194 195 - max_width is an integer, specifying the maximum width of the table196 - if set to 0, size is unlimited, therefore cells won't be wrapped197 """198 199 self.set_max_width(max_width)200 self._precision = 3201 202 self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \203 Texttable.HEADER204 self.set_chars(['-', '|', '+', '='])205 self.reset()206 207 def reset(self):208 """Reset the instance209 210 - reset rows and header211 """212 213 self._hline_string = None214 self._row_size = None215 self._header = []216 self._rows = []217 return self218 219 def set_max_width(self, max_width):220 """Set the maximum width of the table221 222 - max_width is an integer, specifying the maximum width of the table223 - if set to 0, size is unlimited, therefore cells won't be wrapped224 """225 self._max_width = max_width if max_width > 0 else False226 return self227 228 def set_chars(self, array):229 """Set the characters used to draw lines between rows and columns230 231 - the array should contain 4 fields:232 233 [horizontal, vertical, corner, header]234 235 - default is set to:236 237 ['-', '|', '+', '=']238 """239 240 if len(array) != 4:241 raise ArraySizeError("array should contain 4 characters")242 array = [ x[:1] for x in [ str(s) for s in array ] ]243 (self._char_horiz, self._char_vert,244 self._char_corner, self._char_header) = array245 return self246 247 def set_deco(self, deco):248 """Set the table decoration249 250 - 'deco' can be a combination of:251 252 Texttable.BORDER: Border around the table253 Texttable.HEADER: Horizontal line below the header254 Texttable.HLINES: Horizontal lines between rows255 Texttable.VLINES: Vertical lines between columns256 257 All of them are enabled by default258 259 - example:260 261 Texttable.BORDER | Texttable.HEADER262 """263 264 self._deco = deco265 self._hline_string = None266 return self267 268 def set_header_align(self, array):269 """Set the desired header alignment270 271 - the elements of the array should be either "l", "c" or "r":272 273 * "l": column flushed left274 * "c": column centered275 * "r": column flushed right276 """277 278 self._check_row_size(array)279 self._header_align = array280 return self281 282 def set_cols_align(self, array):283 """Set the desired columns alignment284 285 - the elements of the array should be either "l", "c" or "r":286 287 * "l": column flushed left288 * "c": column centered289 * "r": column flushed right290 """291 292 self._check_row_size(array)293 self._align = array294 return self295 296 def set_cols_valign(self, array):297 """Set the desired columns vertical alignment298 299 - the elements of the array should be either "t", "m" or "b":300 301 * "t": column aligned on the top of the cell302 * "m": column aligned on the middle of the cell303 * "b": column aligned on the bottom of the cell304 """305 306 self._check_row_size(array)307 self._valign = array308 return self309 310 def set_cols_dtype(self, array):311 """Set the desired columns datatype for the cols.312 313 - the elements of the array should be either a callable or any of314 "a", "t", "f", "e", "i" or "b":315 316 * "a": automatic (try to use the most appropriate datatype)317 * "t": treat as text318 * "f": treat as float in decimal format319 * "e": treat as float in exponential format320 * "i": treat as int321 * "b": treat as boolean322 * a callable: should return formatted string for any value given323 324 - by default, automatic datatyping is used for each column325 """326 327 self._check_row_size(array)328 self._dtype = array329 return self330 331 def set_cols_width(self, array):332 """Set the desired columns width333 334 - the elements of the array should be integers, specifying the335 width of each column. For example:336 337 [10, 20, 5]338 """339 340 self._check_row_size(array)341 try:342 array = list(map(int, array))343 if reduce(min, array) <= 0:344 raise ValueError345 except ValueError:346 sys.stderr.write("Wrong argument in column width specification\n")347 raise348 self._width = array349 return self350 351 def set_precision(self, width):352 """Set the desired precision for float/exponential formats353 354 - width must be an integer >= 0355 356 - default value is set to 3357 """358 359 if not type(width) is int or width < 0:360 raise ValueError('width must be an integer greater then 0')361 self._precision = width362 return self363 364 def header(self, array):365 """Specify the header of the table366 """367 368 self._check_row_size(array)369 self._header = list(map(obj2unicode, array))370 return self371 372 def add_row(self, array):373 """Add a row in the rows stack374 375 - cells can contain newlines and tabs376 """377 378 self._check_row_size(array)379 380 if not hasattr(self, "_dtype"):381 self._dtype = ["a"] * self._row_size382 383 cells = []384 for i, x in enumerate(array):385 cells.append(self._str(i, x))386 self._rows.append(cells)387 return self388 389 def add_rows(self, rows, header=True):390 """Add several rows in the rows stack391 392 - The 'rows' argument can be either an iterator returning arrays,393 or a by-dimensional array394 - 'header' specifies if the first row should be used as the header395 of the table396 """397 398 # nb: don't use 'iter' on by-dimensional arrays, to get a399 # usable code for python 2.1400 if header:401 if hasattr(rows, '__iter__') and hasattr(rows, 'next'):402 self.header(rows.next())403 else:404 self.header(rows[0])405 rows = rows[1:]406 for row in rows:407 self.add_row(row)408 return self409 410 def draw(self):411 """Draw the table412 413 - the table is returned as a whole string414 """415 416 if not self._header and not self._rows:417 return418 self._compute_cols_width()419 self._check_align()420 out = ""421 if self._has_border():422 out += self._hline()423 if self._header:424 out += self._draw_line(self._header, isheader=True)425 if self._has_header():426 out += self._hline_header()427 length = 0428 for row in self._rows:429 length += 1430 out += self._draw_line(row)431 if self._has_hlines() and length < len(self._rows):432 out += self._hline()433 if self._has_border():434 out += self._hline()435 return out[:-1]436 437 @classmethod438 def _to_float(cls, x):439 if x is None:440 raise FallbackToText()441 try:442 return float(x)443 except (TypeError, ValueError):444 raise FallbackToText()445 446 @classmethod447 def _fmt_int(cls, x, **kw):448 """Integer formatting class-method.449 """450 if type(x) == int:451 return str(x)452 else:453 return str(int(round(cls._to_float(x))))454 455 @classmethod456 def _fmt_float(cls, x, **kw):457 """Float formatting class-method.458 459 - x parameter is ignored. Instead kw-argument f being x float-converted460 will be used.461 462 - precision will be taken from `n` kw-argument.463 """464 n = kw.get('n')465 return '%.*f' % (n, cls._to_float(x))466 467 @classmethod468 def _fmt_exp(cls, x, **kw):469 """Exponential formatting class-method.470 471 - x parameter is ignored. Instead kw-argument f being x float-converted472 will be used.473 474 - precision will be taken from `n` kw-argument.475 """476 n = kw.get('n')477 return '%.*e' % (n, cls._to_float(x))478 479 @classmethod480 def _fmt_text(cls, x, **kw):481 """String formatting class-method."""482 return obj2unicode(x)483 484 @classmethod485 def _fmt_bool(cls, x, **kw):486 """Boolean formatting class-method"""487 return str(bool(x))488 489 @classmethod490 def _fmt_auto(cls, x, **kw):491 """auto formatting class-method."""492 f = cls._to_float(x)493 if abs(f) > 1e8:494 fn = cls._fmt_exp495 elif f != f: # NaN496 fn = cls._fmt_text497 elif f - round(f) == 0:498 fn = cls._fmt_bool if isinstance(x, bool) else cls._fmt_int499 else:500 fn = cls._fmt_float501 return fn(x, **kw)502 503 def _str(self, i, x):504 """Handles string formatting of cell data505 506 i - index of the cell datatype in self._dtype507 x - cell data to format508 """509 FMT = {510 'a':self._fmt_auto,511 'i':self._fmt_int,512 'b':self._fmt_bool,513 'f':self._fmt_float,514 'e':self._fmt_exp,515 't':self._fmt_text,516 }517 518 n = self._precision519 dtype = self._dtype[i]520 try:521 if callable(dtype):522 return dtype(x)523 else:524 return FMT[dtype](x, n=n)525 except FallbackToText:526 return self._fmt_text(x)527 528 def _check_row_size(self, array):529 """Check that the specified array fits the previous rows size530 """531 532 if not self._row_size:533 self._row_size = len(array)534 elif self._row_size != len(array):535 raise ArraySizeError("array should contain %d elements" \536 % self._row_size)537 538 def _has_vlines(self):539 """Return a boolean, if vlines are required or not540 """541 542 return self._deco & Texttable.VLINES > 0543 544 def _has_hlines(self):545 """Return a boolean, if hlines are required or not546 """547 548 return self._deco & Texttable.HLINES > 0549 550 def _has_border(self):551 """Return a boolean, if border is required or not552 """553 554 return self._deco & Texttable.BORDER > 0555 556 def _has_header(self):557 """Return a boolean, if header line is required or not558 """559 560 return self._deco & Texttable.HEADER > 0561 562 def _hline_header(self):563 """Print header's horizontal line564 """565 566 return self._build_hline(True)567 568 def _hline(self):569 """Print an horizontal line570 """571 572 if not self._hline_string:573 self._hline_string = self._build_hline()574 return self._hline_string575 576 def _build_hline(self, is_header=False):577 """Return a string used to separated rows or separate header from578 rows579 """580 horiz = self._char_horiz581 if (is_header):582 horiz = self._char_header583 # compute cell separator584 s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()],585 horiz)586 # build the line587 l = s.join([horiz * n for n in self._width])588 # add border if needed589 if self._has_border():590 l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz,591 self._char_corner)592 else:593 l += "\n"594 return l595 596 def _len_cell(self, cell):597 """Return the width of the cell598 599 Special characters are taken into account to return the width of the600 cell, such like newlines and tabs601 """602 603 cell_lines = cell.split('\n')604 maxi = 0605 for line in cell_lines:606 length = 0607 parts = line.split('\t')608 for part, i in zip(parts, list(range(1, len(parts) + 1))):609 length = length + len(part)610 if i < len(parts):611 length = (length//8 + 1) * 8612 maxi = max(maxi, length)613 return maxi614 615 def _compute_cols_width(self):616 """Return an array with the width of each column617 618 If a specific width has been specified, exit. If the total of the619 columns width exceed the table desired width, another width will be620 computed to fit, and cells will be wrapped.621 """622 623 if hasattr(self, "_width"):624 return625 maxi = []626 if self._header:627 maxi = [ self._len_cell(x) for x in self._header ]628 for row in self._rows:629 for cell,i in zip(row, list(range(len(row)))):630 try:631 maxi[i] = max(maxi[i], self._len_cell(cell))632 except (TypeError, IndexError):633 maxi.append(self._len_cell(cell))634 635 ncols = len(maxi)636 content_width = sum(maxi)637 deco_width = 3*(ncols-1) + [0,4][self._has_border()]638 if self._max_width and (content_width + deco_width) > self._max_width:639 """ content too wide to fit the expected max_width640 let's recompute maximum cell width for each cell641 """642 if self._max_width < (ncols + deco_width):643 raise ValueError('max_width too low to render data')644 available_width = self._max_width - deco_width645 newmaxi = [0] * ncols646 i = 0647 while available_width > 0:648 if newmaxi[i] < maxi[i]:649 newmaxi[i] += 1650 available_width -= 1651 i = (i + 1) % ncols652 maxi = newmaxi653 self._width = maxi654 655 def _check_align(self):656 """Check if alignment has been specified, set default one if not657 """658 659 if not hasattr(self, "_header_align"):660 self._header_align = ["c"] * self._row_size661 if not hasattr(self, "_align"):662 self._align = ["l"] * self._row_size663 if not hasattr(self, "_valign"):664 self._valign = ["t"] * self._row_size665 666 def _draw_line(self, line, isheader=False):667 """Draw a line668 669 Loop over a single cell length, over all the cells670 """671 672 line = self._splitit(line, isheader)673 space = " "674 out = ""675 for i in range(len(line[0])):676 if self._has_border():677 out += "%s " % self._char_vert678 length = 0679 for cell, width, align in zip(line, self._width, self._align):680 length += 1681 cell_line = cell[i]682 fill = width - len(cell_line)683 if isheader:684 align = self._header_align[length - 1]685 if align == "r":686 out += fill * space + cell_line687 elif align == "c":688 out += (int(fill/2) * space + cell_line \689 + int(fill/2 + fill%2) * space)690 else:691 out += cell_line + fill * space692 if length < len(line):693 out += " %s " % [space, self._char_vert][self._has_vlines()]694 out += "%s\n" % ['', space + self._char_vert][self._has_border()]695 return out696 697 def _splitit(self, line, isheader):698 """Split each element of line to fit the column width699 700 Each element is turned into a list, result of the wrapping of the701 string to the desired width702 """703 704 line_wrapped = []705 for cell, width in zip(line, self._width):706 array = []707 for c in cell.split('\n'):708 if c.strip() == "":709 array.append("")710 else:711 array.extend(textwrapper(c, width))712 line_wrapped.append(array)713 max_cell_lines = reduce(max, list(map(len, line_wrapped)))714 for cell, valign in zip(line_wrapped, self._valign):715 if isheader:716 valign = "t"717 if valign == "m":718 missing = max_cell_lines - len(cell)719 cell[:0] = [""] * int(missing / 2)720 cell.extend([""] * int(missing / 2 + missing % 2))721 elif valign == "b":722 cell[:0] = [""] * (max_cell_lines - len(cell))723 else:724 cell.extend([""] * (max_cell_lines - len(cell)))725 return line_wrapped726 727 728if __name__ == '__main__':729 table = Texttable()730 table.set_cols_align(["l", "r", "c"])731 table.set_cols_valign(["t", "m", "b"])732 table.add_rows([["Name", "Age", "Nickname"],733 ["Mr\nXavier\nHuon", 32, "Xav'"],734 ["Mr\nBaptiste\nClement", 1, "Baby"],735 ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"]])736 print(table.draw())737 print()738 739 table = Texttable()740 table.set_deco(Texttable.HEADER)741 table.set_cols_dtype(['t', # text742 'f', # float (decimal)743 'e', # float (exponent)744 'i', # integer745 'a']) # automatic746 table.set_cols_align(["l", "r", "r", "r", "l"])747 table.add_rows([["text", "float", "exp", "int", "auto"],748 ["abcd", "67", 654, 89, 128.001],749 ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023],750 ["lmn", 5e-78, 5e-78, 89.4, .000000000000128],751 ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]])752 print(table.draw())753 