bug fixes and add text report
diff --git a/wally/texttable.py b/wally/texttable.py
new file mode 100644
index 0000000..1917663
--- /dev/null
+++ b/wally/texttable.py
@@ -0,0 +1,586 @@
+# 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
+        """
+        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())