1
0
Эх сурвалжийг харах

Add texttable compat module that is py3k compatible

Christopher Grebs 11 жил өмнө
parent
commit
c6e91db32f

+ 2 - 1
fig/cli/formatter.py

@@ -1,8 +1,9 @@
 from __future__ import unicode_literals
 from __future__ import absolute_import
-import texttable
 import os
 
+from fig.compat import texttable
+
 
 class Formatter(object):
     def table(self, headers, rows):

+ 0 - 0
fig/compat.py → fig/compat/__init__.py


+ 579 - 0
fig/compat/texttable.py

@@ -0,0 +1,579 @@
+#!/usr/bin/env python
+#
+# texttable - module for creating simple ASCII tables
+# Copyright (C) 2003-2011 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"] ])
+    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   |
+    +----------+-----+----------+
+
+    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
+"""
+
+__all__ = ["Texttable", "ArraySizeError"]
+
+__author__ = 'Gerome Fournier <jef(at)foutaise.org>'
+__license__ = 'LGPL'
+__version__ = '0.8.1'
+__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
+"""
+
+# Modified version of `texttable` for python3 support.
+
+import sys
+import string
+
+
+def len(iterable):
+    """Redefining len here so it will be able to work with non-ASCII characters
+    """
+    if not isinstance(iterable, str):
+        return iterable.__len__()
+
+    try:
+        return len(unicode(iterable, 'utf'))
+    except:
+        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.set_chars(['-', '|', '+', '='])
+        self.reset()
+
+    def reset(self):
+        """Reset the instance
+
+        - reset rows and header
+        """
+
+        self._hline_string = None
+        self._row_size = None
+        self._header = []
+        self._rows = []
+
+    def set_chars(self, array):
+        """Set the characters used to draw lines between rows and columns
+
+        - the array should contain 4 fields:
+
+            [horizontal, vertical, corner, header]
+
+        - default is set to:
+
+            ['-', '|', '+', '=']
+        """
+
+        if len(array) != 4:
+            raise ArraySizeError("array should contain 4 characters")
+        array = [ x[:1] for x in [ str(s) for s in array ] ]
+        (self._char_horiz, self._char_vert,
+            self._char_corner, self._char_header) = array
+
+    def set_deco(self, deco):
+        """Set the table decoration
+
+        - 'deco' can be a combinaison of:
+
+            Texttable.BORDER: Border around the table
+            Texttable.HEADER: Horizontal line below the header
+            Texttable.HLINES: Horizontal lines between rows
+            Texttable.VLINES: Vertical lines between columns
+
+           All of them are enabled by default
+
+        - example:
+
+            Texttable.BORDER | Texttable.HEADER
+        """
+
+        self._deco = deco
+
+    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 = 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 = map(str, 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()
+        if self._header:
+            out += self._draw_line(self._header, isheader=True)
+            if self._has_header():
+                out += self._hline_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()
+        if self._has_border():
+            out += self._hline()
+        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 str(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 str(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_header(self):
+        """Print header's horizontal line
+        """
+
+        return self._build_hline(True)
+
+    def _hline(self):
+        """Print an horizontal line
+        """
+
+        if not self._hline_string:
+            self._hline_string = self._build_hline()
+        return self._hline_string
+
+    def _build_hline(self, is_header=False):
+        """Return a string used to separated rows or separate header from
+        rows
+        """
+        horiz = self._char_horiz
+        if (is_header):
+            horiz = self._char_header
+        # compute cell separator
+        s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()],
+            horiz)
+        # build the line
+        l = string.join([horiz * n for n in self._width], s)
+        # add border if needed
+        if self._has_border():
+            l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz,
+                self._char_corner)
+        else:
+            l += "\n"
+        return l
+
+    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, 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, 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 = reduce(lambda x,y: x+y, maxi)
+        if self._max_width and length + items * 3 + 1 > self._max_width:
+            maxi = [(self._max_width - items * 3 -1) / items \
+                for n in range(items)]
+        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 " % (fill/2 * space + cell_line \
+                            + (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'):
+                try:
+                    c = unicode(c, 'utf')
+                except UnicodeDecodeError as strerror:
+                    sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror))
+                    c = unicode(c, 'utf', 'replace')
+                array.extend(textwrap.wrap(c, width))
+            line_wrapped.append(array)
+        max_cell_lines = reduce(max, 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] = [""] * (missing / 2)
+                cell.extend([""] * (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"] ])
+    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())
+

+ 0 - 1
setup.py

@@ -37,7 +37,6 @@ setup(
         'docker-py==0.2.3',
         'docopt==0.6.1',
         'PyYAML==3.10',
-        'texttable==0.8.1',
         # unfortunately `docker` requires six ==1.3.0
         'six==1.3.0',
     ],