| # texttable - module for creating simple ASCII tables |
| # Copyright (C) 2003-2015 Gerome Fournier <jef(at)foutaise.org> |
| # |
| # This library is free software; you can redistribute it and/or |
| # modify it under the terms of the GNU Lesser General Public |
| # License as published by the Free Software Foundation; either |
| # version 2.1 of the License, or (at your option) any later version. |
| # |
| # This library is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # Lesser General Public License for more details. |
| # |
| # You should have received a copy of the GNU Lesser General Public |
| # License along with this library; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
| |
| """module for creating simple ASCII tables |
| |
| |
| Example: |
| |
| table = Texttable() |
| table.set_cols_align(["l", "r", "c"]) |
| table.set_cols_valign(["t", "m", "b"]) |
| table.add_rows([["Name", "Age", "Nickname"], |
| ["Mr\\nXavier\\nHuon", 32, "Xav'"], |
| ["Mr\\nBaptiste\\nClement", 1, "Baby"], |
| ["Mme\\nLouise\\nBourgeau", 28, "Lou\\n\\nLoue"]]) |
| print table.draw() + "\\n" |
| |
| table = Texttable() |
| table.set_deco(Texttable.HEADER) |
| table.set_cols_dtype(['t', # text |
| 'f', # float (decimal) |
| 'e', # float (exponent) |
| 'i', # integer |
| 'a']) # automatic |
| table.set_cols_align(["l", "r", "r", "r", "l"]) |
| table.add_rows([["text", "float", "exp", "int", "auto"], |
| ["abcd", "67", 654, 89, 128.001], |
| ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], |
| ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], |
| ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) |
| print table.draw() |
| |
| Result: |
| |
| +----------+-----+----------+ |
| | Name | Age | Nickname | |
| +==========+=====+==========+ |
| | Mr | | | |
| | Xavier | 32 | | |
| | Huon | | Xav' | |
| +----------+-----+----------+ |
| | Mr | | | |
| | Baptiste | 1 | | |
| | Clement | | Baby | |
| +----------+-----+----------+ |
| | Mme | | Lou | |
| | Louise | 28 | | |
| | Bourgeau | | Loue | |
| +----------+-----+----------+ |
| |
| text float exp int auto |
| =========================================== |
| abcd 67.000 6.540e+02 89 128.001 |
| efgh 67.543 6.540e-01 90 1.280e+22 |
| ijkl 0.000 5.000e-78 89 0.000 |
| mnop 0.023 5.000e+78 92 1.280e+22 |
| """ |
| |
| from __future__ import division |
| |
| __all__ = ["Texttable", "ArraySizeError"] |
| |
| __author__ = 'Gerome Fournier <jef(at)foutaise.org>' |
| __license__ = 'LGPL' |
| __version__ = '0.8.8' |
| __credits__ = """\ |
| Jeff Kowalczyk: |
| - textwrap improved import |
| - comment concerning header output |
| |
| Anonymous: |
| - add_rows method, for adding rows in one go |
| |
| Sergey Simonenko: |
| - redefined len() function to deal with non-ASCII characters |
| |
| Roger Lew: |
| - columns datatype specifications |
| |
| Brian Peterson: |
| - better handling of unicode errors |
| |
| Frank Sachsenheim: |
| - add Python 2/3-compatibility |
| |
| Maximilian Hils: |
| - fix minor bug for Python 3 compatibility |
| |
| frinkelpi: |
| - preserve empty lines |
| """ |
| |
| import sys |
| import string |
| import unicodedata |
| |
| try: |
| if sys.version >= '2.3': |
| import textwrap |
| elif sys.version >= '2.2': |
| from optparse import textwrap |
| else: |
| from optik import textwrap |
| except ImportError: |
| sys.stderr.write("Can't import textwrap module!\n") |
| raise |
| |
| if sys.version >= '2.7': |
| from functools import reduce |
| |
| if sys.version >= '3.0': |
| unicode_type = str |
| bytes_type = bytes |
| else: |
| unicode_type = unicode |
| bytes_type = str |
| |
| |
| def obj2unicode(obj): |
| """Return a unicode representation of a python object |
| """ |
| if isinstance(obj, unicode_type): |
| return obj |
| elif isinstance(obj, bytes_type): |
| try: |
| return unicode_type(obj, 'utf-8') |
| except UnicodeDecodeError as strerror: |
| sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (obj, strerror)) |
| return unicode_type(obj, 'utf-8', 'replace') |
| else: |
| return unicode_type(obj) |
| |
| |
| def len(iterable): |
| """Redefining len here so it will be able to work with non-ASCII characters |
| """ |
| if isinstance(iterable, bytes_type) or isinstance(iterable, unicode_type): |
| unicode_data = obj2unicode(iterable) |
| if hasattr(unicodedata, 'east_asian_width'): |
| w = unicodedata.east_asian_width |
| return sum([w(c) in 'WF' and 2 or 1 for c in unicode_data]) |
| else: |
| return unicode_data.__len__() |
| else: |
| return iterable.__len__() |
| |
| |
| class ArraySizeError(Exception): |
| """Exception raised when specified rows don't fit the required size |
| """ |
| |
| def __init__(self, msg): |
| self.msg = msg |
| Exception.__init__(self, msg, '') |
| |
| def __str__(self): |
| return self.msg |
| |
| |
| class Texttable: |
| |
| BORDER = 1 |
| HEADER = 1 << 1 |
| HLINES = 1 << 2 |
| VLINES = 1 << 3 |
| |
| def __init__(self, max_width=80): |
| """Constructor |
| |
| - max_width is an integer, specifying the maximum width of the table |
| - if set to 0, size is unlimited, therefore cells won't be wrapped |
| """ |
| |
| if max_width <= 0: |
| max_width = False |
| self._max_width = max_width |
| self._precision = 3 |
| |
| self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | Texttable.HEADER |
| self._reset() |
| ## left, horiz, cross, right |
| self._chars_top = (chr(0x250c), chr(0x2500), chr(0x252c), chr(0x2510)) |
| # self.chars_header = (chr(0x255e), chr(0x2550), chr(0x256a), chr(0x2561)) |
| self._chars_header = (chr(0x251d), chr(0x2501), chr(0x253f), chr(0x2525)) |
| self._chars_middle = (chr(0x251c), chr(0x2500), chr(0x253c), chr(0x2524)) |
| self._chars_bottom = (chr(0x2514), chr(0x2500), chr(0x2534), chr(0x2518)) |
| self._char_vert = chr(0x2502) |
| self._align = None |
| |
| def _reset(self): |
| """Reset the instance |
| |
| - reset rows and header |
| """ |
| |
| self._row_size = None |
| self._header = [] |
| self._rows = [] |
| |
| def set_cols_align(self, array): |
| """Set the desired columns alignment |
| |
| - the elements of the array should be either "l", "c" or "r": |
| |
| * "l": column flushed left |
| * "c": column centered |
| * "r": column flushed right |
| """ |
| |
| self._check_row_size(array) |
| self._align = array |
| |
| def set_cols_valign(self, array): |
| """Set the desired columns vertical alignment |
| |
| - the elements of the array should be either "t", "m" or "b": |
| |
| * "t": column aligned on the top of the cell |
| * "m": column aligned on the middle of the cell |
| * "b": column aligned on the bottom of the cell |
| """ |
| |
| self._check_row_size(array) |
| self._valign = array |
| |
| def set_cols_dtype(self, array): |
| """Set the desired columns datatype for the cols. |
| |
| - the elements of the array should be either "a", "t", "f", "e" or "i": |
| |
| * "a": automatic (try to use the most appropriate datatype) |
| * "t": treat as text |
| * "f": treat as float in decimal format |
| * "e": treat as float in exponential format |
| * "i": treat as int |
| |
| - by default, automatic datatyping is used for each column |
| """ |
| |
| self._check_row_size(array) |
| self._dtype = array |
| |
| def set_cols_width(self, array): |
| """Set the desired columns width |
| |
| - the elements of the array should be integers, specifying the |
| width of each column. For example: |
| |
| [10, 20, 5] |
| """ |
| |
| self._check_row_size(array) |
| try: |
| array = list(map(int, array)) |
| if reduce(min, array) <= 0: |
| raise ValueError |
| except ValueError: |
| sys.stderr.write("Wrong argument in column width specification\n") |
| raise |
| self._width = array |
| |
| def set_precision(self, width): |
| """Set the desired precision for float/exponential formats |
| |
| - width must be an integer >= 0 |
| |
| - default value is set to 3 |
| """ |
| |
| if not type(width) is int or width < 0: |
| raise ValueError('width must be an integer greater then 0') |
| self._precision = width |
| |
| def header(self, array): |
| """Specify the header of the table |
| """ |
| |
| self._check_row_size(array) |
| self._header = list(map(obj2unicode, array)) |
| |
| def add_row(self, array): |
| """Add a row in the rows stack |
| |
| - cells can contain newlines and tabs |
| """ |
| |
| self._check_row_size(array) |
| |
| if not hasattr(self, "_dtype"): |
| self._dtype = ["a"] * self._row_size |
| |
| cells = [] |
| for i, x in enumerate(array): |
| cells.append(self._str(i, x)) |
| |
| self._rows.append(cells) |
| |
| def add_rows(self, rows, header=True): |
| """Add several rows in the rows stack |
| |
| - The 'rows' argument can be either an iterator returning arrays, |
| or a by-dimensional array |
| - 'header' specifies if the first row should be used as the header |
| of the table |
| """ |
| |
| # nb: don't use 'iter' on by-dimensional arrays, to get a |
| # usable code for python 2.1 |
| if header: |
| if hasattr(rows, '__iter__') and hasattr(rows, 'next'): |
| self.header(rows.next()) |
| else: |
| self.header(rows[0]) |
| rows = rows[1:] |
| for row in rows: |
| self.add_row(row) |
| |
| def draw(self): |
| """Draw the table |
| |
| - the table is returned as a whole string |
| """ |
| |
| if not self._header and not self._rows: |
| return |
| self._compute_cols_width() |
| self._check_align() |
| out = "" |
| |
| if self._has_border(): |
| out += self._hline(*self._chars_top) |
| |
| if self._header: |
| out += self._draw_line(self._header, isheader=True) |
| if self._has_header(): |
| out += self._hline(*self._chars_header) |
| |
| length = 0 |
| for row in self._rows: |
| length += 1 |
| out += self._draw_line(row) |
| if self._has_hlines() and length < len(self._rows): |
| out += self._hline(*self._chars_middle) |
| |
| if self._has_border(): |
| out += self._hline(*self._chars_bottom) |
| |
| return out[:-1] |
| |
| def _str(self, i, x): |
| """Handles string formatting of cell data |
| |
| i - index of the cell datatype in self._dtype |
| x - cell data to format |
| """ |
| if isinstance(x, str): |
| return x |
| |
| try: |
| f = float(x) |
| except: |
| return obj2unicode(x) |
| |
| n = self._precision |
| dtype = self._dtype[i] |
| |
| if dtype == 'i': |
| return str(int(round(f))) |
| elif dtype == 'f': |
| return '%.*f' % (n, f) |
| elif dtype == 'e': |
| return '%.*e' % (n, f) |
| elif dtype == 't': |
| return obj2unicode(x) |
| else: |
| if f - round(f) == 0: |
| if abs(f) > 1e8: |
| return '%.*e' % (n, f) |
| else: |
| return str(int(round(f))) |
| else: |
| if abs(f) > 1e8: |
| return '%.*e' % (n, f) |
| else: |
| return '%.*f' % (n, f) |
| |
| def _check_row_size(self, array): |
| """Check that the specified array fits the previous rows size |
| """ |
| |
| if not self._row_size: |
| self._row_size = len(array) |
| elif self._row_size != len(array): |
| raise ArraySizeError("array should contain %d elements" \ |
| % self._row_size) |
| |
| def _has_vlines(self): |
| """Return a boolean, if vlines are required or not |
| """ |
| |
| return self._deco & Texttable.VLINES > 0 |
| |
| def _has_hlines(self): |
| """Return a boolean, if hlines are required or not |
| """ |
| |
| return self._deco & Texttable.HLINES > 0 |
| |
| def _has_border(self): |
| """Return a boolean, if border is required or not |
| """ |
| |
| return self._deco & Texttable.BORDER > 0 |
| |
| def _has_header(self): |
| """Return a boolean, if header line is required or not |
| """ |
| |
| return self._deco & Texttable.HEADER > 0 |
| |
| def _hline(self, left, horiz, cross, right): |
| """Return a string used to separated rows or separate header from rows""" |
| |
| # compute cell separator |
| sep = horiz + (cross if self._has_vlines() else horiz) + horiz |
| |
| # build the line |
| line = sep.join([horiz * n for n in self._width]) |
| |
| # add border if needed |
| if self._has_border(): |
| line = left + horiz + line + horiz + right |
| |
| return line + "\n" |
| |
| def _len_cell(self, cell): |
| """Return the width of the cell |
| |
| Special characters are taken into account to return the width of the |
| cell, such like newlines and tabs |
| """ |
| |
| cell_lines = cell.split('\n') |
| maxi = 0 |
| for line in cell_lines: |
| length = 0 |
| parts = line.split('\t') |
| for part, i in zip(parts, list(range(1, len(parts) + 1))): |
| length = length + len(part) |
| if i < len(parts): |
| length = (length//8 + 1) * 8 |
| maxi = max(maxi, length) |
| return maxi |
| |
| def _compute_cols_width(self): |
| """Return an array with the width of each column |
| |
| If a specific width has been specified, exit. If the total of the |
| columns width exceed the table desired width, another width will be |
| computed to fit, and cells will be wrapped. |
| """ |
| |
| if hasattr(self, "_width"): |
| return |
| maxi = [] |
| if self._header: |
| maxi = [ self._len_cell(x) for x in self._header ] |
| for row in self._rows: |
| for cell,i in zip(row, list(range(len(row)))): |
| try: |
| maxi[i] = max(maxi[i], self._len_cell(cell)) |
| except (TypeError, IndexError): |
| maxi.append(self._len_cell(cell)) |
| items = len(maxi) |
| length = sum(maxi) |
| if self._max_width and length + items * 3 + 1 > self._max_width: |
| maxi = [ |
| int(round(self._max_width / (length + items * 3 + 1) * n)) |
| for n in maxi |
| ] |
| self._width = maxi |
| |
| def _check_align(self): |
| """Check if alignment has been specified, set default one if not |
| """ |
| |
| if not hasattr(self, "_align"): |
| self._align = ["l"] * self._row_size |
| if not hasattr(self, "_valign"): |
| self._valign = ["t"] * self._row_size |
| |
| def _draw_line(self, line, isheader=False): |
| """Draw a line |
| |
| Loop over a single cell length, over all the cells |
| """ |
| |
| line = self._splitit(line, isheader) |
| space = " " |
| out = "" |
| for i in range(len(line[0])): |
| if self._has_border(): |
| out += "%s " % self._char_vert |
| length = 0 |
| for cell, width, align in zip(line, self._width, self._align): |
| length += 1 |
| cell_line = cell[i] |
| fill = width - len(cell_line) |
| if isheader: |
| align = "c" |
| if align == "r": |
| out += "%s " % (fill * space + cell_line) |
| elif align == "c": |
| out += "%s " % (int(fill/2) * space + cell_line + int(fill/2 + fill%2) * space) |
| else: |
| out += "%s " % (cell_line + fill * space) |
| if length < len(line): |
| out += "%s " % [space, self._char_vert][self._has_vlines()] |
| out += "%s\n" % ['', self._char_vert][self._has_border()] |
| return out |
| |
| def _splitit(self, line, isheader): |
| """Split each element of line to fit the column width |
| |
| Each element is turned into a list, result of the wrapping of the |
| string to the desired width |
| """ |
| |
| line_wrapped = [] |
| for cell, width in zip(line, self._width): |
| array = [] |
| for c in cell.split('\n'): |
| if c.strip() == "": |
| array.append("") |
| else: |
| array.extend(textwrap.wrap(c, width)) |
| line_wrapped.append(array) |
| max_cell_lines = reduce(max, list(map(len, line_wrapped))) |
| for cell, valign in zip(line_wrapped, self._valign): |
| if isheader: |
| valign = "t" |
| if valign == "m": |
| missing = max_cell_lines - len(cell) |
| cell[:0] = [""] * int(missing / 2) |
| cell.extend([""] * int(missing / 2 + missing % 2)) |
| elif valign == "b": |
| cell[:0] = [""] * (max_cell_lines - len(cell)) |
| else: |
| cell.extend([""] * (max_cell_lines - len(cell))) |
| return line_wrapped |
| |
| |
| if __name__ == '__main__': |
| table = Texttable() |
| table.set_cols_align(["l", "r", "c"]) |
| table.set_cols_valign(["t", "m", "b"]) |
| table.add_rows([["Name", "Age", "Nickname"], |
| ["Mr\nXavier\nHuon", 32, "Xav'"], |
| ["Mr\nBaptiste\nClement", 1, "Baby"], |
| ["Mme\nLouise\nBourgeau", 28, "Lou\n \nLoue"]]) |
| print(table.draw() + "\n") |
| |
| table = Texttable() |
| table.set_deco(Texttable.HEADER) |
| table.set_cols_dtype(['t', # text |
| 'f', # float (decimal) |
| 'e', # float (exponent) |
| 'i', # integer |
| 'a']) # automatic |
| table.set_cols_align(["l", "r", "r", "r", "l"]) |
| table.add_rows([["text", "float", "exp", "int", "auto"], |
| ["abcd", "67", 654, 89, 128.001], |
| ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], |
| ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], |
| ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) |
| print(table.draw()) |