Source code for easytable

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright notice
# ----------------
#
# Copyright (C) 2014 Daniel Jung
# Contact: djungbremen@gmail.com
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
#
"""Create table representations from various data structures.

Various data structures are considered: Lists containing lists, dictionaries
containing lists, lists containing dictionaries etc. The idea of this package
is to find the natural table-like representation for each of the considered
data structures.

The function :func:`autotable` is provided that can infer an appropiate table
form the given data structure. :func:`autotable` is the preferred function to
generate the table representation of most kind of tables.

:func:`autotable` is choosing one of the following specialized functions which
is appropiate for the kind of data structure that has been passed to it.  The
goal is to support of as many data structures as possible. The special
functions can also used directly, to force a certain representation:

    :func:`docl`:
        dictionary of lists, where each list represents a named **column**

    :func:`dorl`:
        dictionary of lists, where each list represents a named **row**

    :func:`locl`:
        list of lists, where each list represents a **column**

    :func:`lorl`:
        list of lists, where each list represents a **row**

    :func:`locd`:
        list of dictionaries, where each dictionary represents a **column**

    :func:`lord`:
        list of dictionaries, where each dictionary represents a **row**

    :func:`docd`:
        dictionary of dictionaries, where each dictionary represents a named
        **column**, and each key in the latter defines the name of the **row**

    :func:`dord`:
        dictionary of dictionaries, where each dictionary represents a named
        **row**, and each key in the latter defines the name of the **column**

For absolute fine control, the :class:`Table` class can be used directly to
construct the table by hand.  The method :meth:`Table.make` offers a lot of
options to format the table. For a complete list, refer to the documentation of
:meth:`Table.make`. The respective keyword arguments can also be passed through
the shortcut functions :func:`autotable`, :func:`docl` etc. A few formatting
presets can be found in the submodule :mod:`~easytable.presets`.

At the moment, there are no ambitions to implement any sorting or filtering
options.  Data structures have to be passed in an already ordered way. **Tip:**
In the case of dictionaries, the class :class:`collections.OrderedDict` can be
used to force a certain row or column order in the table output."""
#
# 2013-08-12 - 2013-08-13

import presets

"""

Conceptual thoughts
-------------------

Which possibilities do we have?

Say, a list of lists is given. What can we do with it to present it in a
reasonable way?
--> Either it is a list of rows or a list of columns.    locl, lorl

Then, if a list of dictionaries is given, what can we do?
--> Either it is a list of rows, with names columns, or a list of columns, with
    named rows.                                          lord, locd

How about a dictionary of lists?
--> Either it is a dictionary of named rows or a dictionary of named columns.
                                                         dorl, docl

If a dictionary of dictionaries is given:
--> Dictionary of named rows, for which the columns are named as well.
                                                         dord, docd

If only one dictionary is given, and the keys are 2-tuples:
--> Each 2-tuple contains the names of row and of column (like indices), and
    the data is arranged accordingly in a sparse matrix style.
                                                         do2t

If three lists are given, the first could contain the row index, the second the
column index, and the third the respective data value.

Sets can be treated like lists, but the elements will be unordered. (The
responsibility remains with the user to first make a sorted list out of it.)
Frozen sets and tuples are of course treated like their ``unfrozen''
counterparts.

There could be a general function "autotable" that chooses an appropriate table
format automatically for a given data structure.

All functions return just the string that would draw the table on the screen if
printed. The terminal width can be set to cut the table.

Could colors play some role? Should the module :mod:`ansicolor` be used to
compute the actual length of the strings? (unwanted dependency...)

A remapping of the row and column titles could be done.

A LaTeX output mode could be implemented (as well as a HTML mode).

Numbers should be formattable and right-aligned.

There could be support to print tree-like (hierarchical) data structures
(although it is not really a table).

Think about configurable row and column separators, major as well as minor
ones, margin and padding, and frames.

Option to print titles in uppercase? No, the user can do that himself.

collections.OrderedDict can be used to force a specific row or column order
when using dictionaries. No sorting options are neccessary.

"""


DEFAULT_ALIGN = 'left'


[docs]def autotable(data_structure, **kwargs): """Generate table representation of the given data structure. Choose one of the specialized functions *docl*, *dorl*, etc. based on the given data structure. Keyword arguments are passed to :py:meth:`Table.make`.""" if isdict(data_structure): if alldict(data_structure.values()): # decide if rowwise or columnwise is best func1 = docd func2 = dord elif alliter(data_structure.values()): # decide if rowwise or columnwise is best func1 = docl func2 = dorl else: raise TypeError('must contain dictionaries or iterables') elif isiter(data_structure): if alldict(data_structure): # decide if rowwise or columnwise is best func1 = locd func2 = lord elif alliter(data_structure): # decide if rowwise or columnwise is best func1 = locl func2 = lorl else: raise TypeError('must contain dictionaries or iterables') else: raise TypeError('must be of type dict or iterable') s1 = func1(data_structure, **kwargs) s2 = func2(data_structure, **kwargs) width1 = len(s1.split('\n')[0]) width2 = len(s2.split('\n')[0]) if width1 > 80 and width2 <= 80: func = func2 elif width2 > 80 and width1 <= 80: func = func1 elif width1 > 80 and width2 > 80: if width1 > width2: func = func2 else: func = func1 else: if width1 > width2: func = func1 else: func = func2 return func(data_structure, **kwargs)
[docs]def docl(dict_of_lists, **kwargs): """Return table representation of the given dictionary of lists, where each list represents a named **column**. Keyword arguments are passed to :py:meth:`Table.make`.""" table = Table() for title, data in dict_of_lists.iteritems(): table.append_column(data, title) return table.make(**kwargs)
[docs]def dorl(dict_of_lists, **kwargs): """Return table representation of the given dictionary of lists, where each list represents a named **row**. Keyword arguments are passed to :py:meth:`Table.make`.""" table = Table() for title, data in dict_of_lists.iteritems(): table.append_row(data, title) return table.make(**kwargs)
[docs]def locl(list_of_lists, **kwargs): """Return table representation of the given list of lists, where each list represents a **column**. Keyword arguments are passed to :py:meth:`Table.make`.""" table = Table() for data in list_of_lists: table.append_column(data) return table.make(**kwargs)
[docs]def lorl(list_of_lists, **kwargs): """Return table representation of the given list of lists, where each list represents a **row**. Keyword arguments are passed to :py:meth:`Table.make`.""" table = Table() for data in list_of_lists: table.append_row(data) return table.make(**kwargs)
[docs]def locd(list_of_dicts, **kwargs): """Return table representation of the given list of dictionaries, where each dictionary represents a **column**. Keyword arguments are passed to :py:meth:`Table.make`.""" allrowtitles = [] for coldata in list_of_dicts: for rowtitle in coldata.keys(): if not rowtitle in allrowtitles: allrowtitles.append(rowtitle) table = Table() for colind, coldata in enumerate(list_of_dicts): for rowind, rowtitle in enumerate(allrowtitles): if rowtitle in coldata: table.insert_cell((rowind, colind), coldata[rowtitle]) for rowind, rowtitle in enumerate(allrowtitles): table.rowtitles[rowind] = rowtitle return table.make(**kwargs)
[docs]def lord(list_of_dicts, **kwargs): """Return table representation of the given list of dictionaries, where each dictionary represents a **row**. Keyword arguments are passed to :py:meth:`Table.make`.""" allcoltitles = [] for rowdata in list_of_dicts: for coltitle in rowdata.keys(): if not coltitle in allcoltitles: allcoltitles.append(coltitle) table = Table() for rowind, rowdata in enumerate(list_of_dicts): for colind, coltitle in enumerate(allcoltitles): if coltitle in rowdata: table.insert_cell((rowind, colind), rowdata[coltitle]) for colind, coltitle in enumerate(allcoltitles): table.coltitles[colind] = coltitle return table.make(**kwargs)
[docs]def docd(dict_of_dicts, **kwargs): """Return table representation of the given dictionary of dictionaries, where each dictionary represents a **column**. Keyword arguments are passed to :py:meth:`Table.make`.""" allrowtitles = [] for coldata in dict_of_dicts.values(): for rowtitle in coldata.keys(): if not rowtitle in allrowtitles: allrowtitles.append(rowtitle) table = Table() allcoltitles = dict_of_dicts.keys() for colind, (coltitle, coldata) in enumerate(dict_of_dicts.iteritems()): table.coltitles[colind] = coltitle for rowtitle, celldata in coldata.iteritems(): rowind = allrowtitles.index(rowtitle) table.insert_cell((rowind, colind), celldata) for rowind, rowtitle in enumerate(allrowtitles): table.rowtitles[rowind] = rowtitle return table.make(**kwargs)
[docs]def dord(dict_of_dicts, **kwargs): """Return table representation of the given dictionary of dictionaries, where each dictionary represents a **row**. Keyword arguments are passed to :py:meth:`Table.make`.""" allcoltitles = [] for rowdata in dict_of_dicts.values(): for coltitle in rowdata.keys(): if not coltitle in allcoltitles: allcoltitles.append(coltitle) table = Table() allrowtitles = dict_of_dicts.keys() for rowind, (rowtitle, rowdata) in enumerate(dict_of_dicts.iteritems()): table.rowtitles[rowind] = rowtitle for coltitle, celldata in rowdata.iteritems(): colind = allcoltitles.index(coltitle) table.insert_cell((rowind, colind), celldata) for colind, coltitle in enumerate(allcoltitles): table.coltitles[colind] = coltitle return table.make(**kwargs)
[docs]def isiter(obj): """Check if an object is iterable. Return True for lists, tuples, dictionaries and numpy arrays (all objects that possess an __iter__ method). Return False for scalars (float, int, etc.), strings, bool and None.""" # 2011-09-13 - 2014-07-20 # copied from tb.misc.isiterable on 2014-07-10 # former tb.isiterable from 2011-01-27 # former mytools.isiterable # Initial idea from # http://bytes.com/topic/python/answers/514838-how-test-if-object-sequence- # iterable: # return isinstance(obj, basestring) or getattr(obj, '__iter__', False) # I found this to be better: return not getattr(obj, '__iter__', False) is False
[docs]def alliter(seq): for item in seq: if not isiter(item): return False return True
[docs]def isdict(obj): """Check if the given object *obj* is a dictionary.""" return hasattr(obj, 'iteritems')
[docs]def alldict(seq): for item in seq: if not isdict(item): return False return True
[docs]class items_of(object): """Instances of this class are callables which get a certain item of each element of a given iterable, and returns all items in form of a new iterable. If item does not exist and a default value is given, return that value.""" # copied from frog.items_of on 2014-07-10 def __init__(self, itemname, default=None, dtype=None): self.itemname = itemname self.default = default self.dtype = dtype def __call__(self, iterable): dtype = self.dtype or type(iterable) newiter = [] for item in iterable: if self.default is not None: try: value = item[self.itemname] except: value = self.default else: value = item[self.itemname] newiter.append(value) return dtype(newiter)
[docs]def all_of_type(seq, dtype): """Check if all items of the given sequence *seq* are of the type *dtype*. *dtype* can also be a list of possible types.""" for item in seq: if not isinstance(item, dtype): return False return True
[docs]class CellDict(dict): """A mapping ``(row index, column index) --> cell object``.""" def __init__(self, mapping_or_iterable=None, **kwargs): self._check_mapping_or_iterable(kwargs) if mapping_or_iterable: self._check_mapping_or_iterable(mapping_or_iterable) dict.__init__(self, mapping_or_iterable, **kwargs) else: dict.__init__(self, **kwargs) def __setitem__(self, key, value): self._check_key(key) self._check_value(value) dict.__setitem__(self, key, value) def __delitem__(self, inds): self.cells[inds].table = None dict.__delitem__(self, key) def _check_key(self, key): if not isiter(key): raise TypeError('key must be iterable') if len(key) != 2: raise ValueError('key must have length 2') if not isinstance(key[0], int) \ or not isinstance(key[1], int): raise TypeError('key must contain integers') def _check_value(self, value): if not isinstance(value, Cell): raise TypeError('value must be of type Cell') def _check_mapping_or_iterable(self, mapping_or_iterable): if isinstance(mapping_or_iterable, dict): for key, value in mapping_or_iterable.iteritems(): self._check_key(key) self._check_value(value) else: for key, value in mapping_or_iterable: self._check_key(key) self._check_value(value)
[docs] def index(self, cell): """Return indices (row, column) of the given cell object. Raise ValueError if the cell is not present.""" if cell not in self.values(): raise ValueError('%s is not in %s' % (cell, self)) for inds, c in self.iteritems(): if c is cell: return inds
[docs]class TitleDict(dict): """A mapping ``integer --> title``. Keys must be integer.""" def __init__(self, mapping_or_iterable=None, **kwargs): self._check_mapping_or_iterable(kwargs) if mapping_or_iterable: self._check_mapping_or_iterable(mapping_or_iterable) dict.__init__(self, mapping_or_iterable, **kwargs) else: dict.__init__(self, **kwargs) def __setitem__(self, key, value): self._check_key(key) dict.__setitem__(self, key, value) def _check_key(self, key): if not isinstance(key, int): raise TypeError('key must be integer') def _check_mapping_or_iterable(self, mapping_or_iterable): if isinstance(mapping_or_iterable, dict): for key, value in mapping_or_iterable.iteritems(): self._check_key(key) else: for key, value in mapping_or_iterable: self._check_key(key)
[docs]class AlignDict(dict): """A mapping ``integer --> string``. Keys must be integer, values must be one of the strings "left", "right", "center", or "point".""" def __init__(self, mapping_or_iterable=None, **kwargs): self._check_mapping_or_iterable(kwargs) if mapping_or_iterable: self._check_mapping_or_iterable(mapping_or_iterable) dict.__init__(self, mapping_or_iterable, **kwargs) else: dict.__init__(self, **kwargs) def __setitem__(self, key, value): self._check_key(key) self._check_value(value) dict.__setitem__(self, key, value) def _check_key(self, key): if not isinstance(key, int): raise TypeError('key must be integer') def _check_value(self, value): msg = 'value must be one of the strings "left", "right", ' + \ '"center", or "point"' if not isinstance(value, basestring): raise TypeError(msg) if value not in ['left', 'right', 'center', 'point']: raise ValueError(msg) def _check_mapping_or_iterable(self, mapping_or_iterable): if isinstance(mapping_or_iterable, dict): for key, value in mapping_or_iterable.iteritems(): self._check_key(key) self._check_value(value) else: for key, value in mapping_or_iterable: self._check_key(key) self._check_value(value)
[docs]class Table(object): def __init__(self, cells=None, rowtitles=None, coltitles=None, colalign=None): self._cells = CellDict() if cells is None else cells self._rowtitles = TitleDict() if rowtitles is None else rowtitles self._coltitles = TitleDict() if coltitles is None else coltitles self._colalign = AlignDict() if colalign is None else colalign @property def cells(self): return self._cells @cells.setter
[docs] def cells(self, celldict): if not isinstance(celldict, CellDict): raise TypeError('must be of type CellDict') self._cells = celldict
@property def rowtitles(self): return self._rowtitles @rowtitles.setter
[docs] def rowtitles(self, titles): if not isinstance(titles, TitleDict): raise TypeError('must be of type TitleDict') self._rowtitles = titles
@property def coltitles(self): return self._coltitles @coltitles.setter
[docs] def coltitles(self, titles): if not isinstance(titles, TitleDict): raise TypeError('must be of type TitleDict') self._coltitles = titles
@property def colalign(self): return self._colalign @colalign.setter
[docs] def colalign(self, alignments): if not isinstance(colalign, AlignDict): raise TypeError('must be of type AlignDict') self._colalign = alignments
[docs] def nrows(self): rowinds = self.rowinds() h = max(rowinds) - min(rowinds) + 1 if rowinds else 0 return h
[docs] def ncols(self): colinds = self.colinds() w = max(colinds) - min(colinds) + 1 if colinds else 0 return w
[docs] def size(self): """Return size of the table in the form (number of rows, number of columns), excluding titles.""" rowinds, colinds = self.rowinds_and_colinds() h = max(rowinds) - min(rowinds) + 1 if rowinds else 0 w = max(colinds) - min(colinds) + 1 if colinds else 0 return h, w
[docs] def top(self): rowinds = self.rowinds() return min(rowinds) if rowinds else None
[docs] def bottom(self): rowinds = self.rowinds() return max(rowinds) if rowinds else None
[docs] def left(self): colinds = self.colinds() return min(colinds) if colinds else None
[docs] def right(self): colinds = self.colinds() return max(colinds) if colinds else None
[docs] def rowinds(self): keys = self.cells.keys() rowinds, colinds = zip(*keys) if keys else ((), ()) rowinds = list(rowinds) rowinds.sort() return rowinds
[docs] def colinds(self): keys = self.cells.keys() rowinds, colinds = zip(*keys) if keys else ((), ()) colinds = list(colinds) colinds.sort() return colinds
[docs] def rowinds_and_colinds(self): keys = self.cells.keys() rowinds, colinds = zip(*keys) if keys else ((), ()) rowinds = list(rowinds) rowinds.sort() colinds = list(colinds) colinds.sort() return rowinds, colinds
[docs] def column(self, index): out = [] for (r, c), cell in self.cells.iteritems(): if c == index: out.append(cell) out.sort(key=lambda cell: cell.row) return out
[docs] def row(self, index): out = [] for (r, c), cell in self.cells.iteritems(): if r == index: out.append(cell) out.sort(key=lambda cell: cell.column) return out
[docs] def insert_cell(self, (row, column), data=None): c = Cell(self, (row, column), data=data) self.cells[(row, column)] = c
[docs] def insert_column(self, index, data, title=None, align=None, startrow=0): colind = index for i, d in enumerate(data): self.insert_cell((startrow+i, colind), d) if title is not None: self.coltitles[colind] = title if align is not None: self.colalign[colind] = align
[docs] def append_column(self, data, title=None, align=None, startrow=0): right = self.right() colind = right+1 if right is not None else 0 self.insert_column(colind, data=data, title=title, align=align, startrow=startrow)
[docs] def insert_row(self, index, data, title=None, startcol=0): rowind = index for i, d in enumerate(data): self.insert_cell((rowind, startcol+i), d) if title is not None: self.rowtitles[rowind] = title
[docs] def append_row(self, data, title=None, startcol=0): bottom = self.bottom() rowind = bottom + 1 if bottom is not None else 0 self.insert_row(rowind, data=data, title=title, startcol=startcol)
_mergedefault = {('|', '-'): '+', ('|', '='): '+', ('|', '~'): '+', (':', '-'): '+', (':', '='): '+', (':', '~'): '+', (';', '-'): '+', (';', '='): '+', (';', '~'): '+', ('!', '-'): '+', ('!', '='): '+', ('!', '~'): '+', ('I', '-'): '+', ('I', '='): '+', ('I', '~'): '+', ('*', '*'): '*', ('#', '#'): '#', ('@', '@'): '@', ('+', '+'): '+', ('/', '/'): '/', ('\\', '\\'): '\\', ('>', '>'): '>', ('<', '<'): '<', ('.', '.'): '.', (':', ':'): ':', (';', ';'): ';', (':', '.'): ':', ('"', '"'): '"', ('z', 'z'): 'z', ('Z', 'Z'): 'Z', ('o', 'o'): 'o', ('O', 'O'): 'O', ('\'', '\''): '\''} _mergetopleft = _mergedefault.copy() _mergetopright = _mergedefault.copy() _mergebottomleft = _mergedefault.copy() _mergebottomright = _mergedefault.copy() _mergeleft = _mergedefault.copy() _mergeright = _mergedefault.copy() _mergetop = _mergedefault.copy() _mergebottom = _mergedefault.copy() _mergecenter = _mergedefault.copy() _pieces_light_horizontal = [unichr(9474), unichr(9478), unichr(9482), unichr(9550)] _pieces_light_vertical = [unichr(9472), unichr(9476), unichr(9480), unichr(9548)] _pieces_heavy_horizontal = [unichr(9475), unichr(9479), unichr(9483), unichr(9551)] _pieces_heavy_vertical = [unichr(9473), unichr(9477), unichr(9481), unichr(9549)] _pieces_double_horizontal = [unichr(9553)] _pieces_double_vertical = [unichr(9552)] for hpiece in _pieces_light_horizontal: for vpiece in _pieces_light_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9484) _mergetopright[(hpiece, vpiece)] = unichr(9488) _mergebottomleft[(hpiece, vpiece)] = unichr(9492) _mergebottomright[(hpiece, vpiece)] = unichr(9496) _mergeleft[(hpiece, vpiece)] = unichr(9500) _mergeright[(hpiece, vpiece)] = unichr(9508) _mergetop[(hpiece, vpiece)] = unichr(9516) _mergebottom[(hpiece, vpiece)] = unichr(9524) _mergecenter[(hpiece, vpiece)] = unichr(9532) for hpiece in _pieces_light_horizontal: for vpiece in _pieces_heavy_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9485) _mergetopright[(hpiece, vpiece)] = unichr(9489) _mergebottomleft[(hpiece, vpiece)] = unichr(9493) _mergebottomright[(hpiece, vpiece)] = unichr(9497) _mergeleft[(hpiece, vpiece)] = unichr(9501) _mergeright[(hpiece, vpiece)] = unichr(9509) _mergetop[(hpiece, vpiece)] = unichr(9519) _mergebottom[(hpiece, vpiece)] = unichr(9527) _mergecenter[(hpiece, vpiece)] = unichr(9535) for hpiece in _pieces_heavy_horizontal: for vpiece in _pieces_light_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9486) _mergetopright[(hpiece, vpiece)] = unichr(9490) _mergebottomleft[(hpiece, vpiece)] = unichr(9494) _mergebottomright[(hpiece, vpiece)] = unichr(9498) _mergeleft[(hpiece, vpiece)] = unichr(9504) _mergeright[(hpiece, vpiece)] = unichr(9512) _mergetop[(hpiece, vpiece)] = unichr(9520) _mergebottom[(hpiece, vpiece)] = unichr(9528) _mergecenter[(hpiece, vpiece)] = unichr(9538) for hpiece in _pieces_heavy_horizontal + _pieces_double_horizontal: for vpiece in _pieces_heavy_vertical + _pieces_double_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9487) _mergetopright[(hpiece, vpiece)] = unichr(9491) _mergebottomleft[(hpiece, vpiece)] = unichr(9495) _mergebottomright[(hpiece, vpiece)] = unichr(9499) _mergeleft[(hpiece, vpiece)] = unichr(9507) _mergeright[(hpiece, vpiece)] = unichr(9515) _mergetop[(hpiece, vpiece)] = unichr(9523) _mergebottom[(hpiece, vpiece)] = unichr(9531) _mergecenter[(hpiece, vpiece)] = unichr(9547) for hpiece in _pieces_double_horizontal: for vpiece in _pieces_double_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9556) _mergetopright[(hpiece, vpiece)] = unichr(9559) _mergebottomleft[(hpiece, vpiece)] = unichr(9562) _mergebottomright[(hpiece, vpiece)] = unichr(9565) _mergeleft[(hpiece, vpiece)] = unichr(9568) _mergeright[(hpiece, vpiece)] = unichr(9571) _mergetop[(hpiece, vpiece)] = unichr(9574) _mergebottom[(hpiece, vpiece)] = unichr(9577) _mergecenter[(hpiece, vpiece)] = unichr(9580) for hpiece in _pieces_double_horizontal: for vpiece in _pieces_light_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9555) _mergetopright[(hpiece, vpiece)] = unichr(9558) _mergebottomleft[(hpiece, vpiece)] = unichr(9561) _mergebottomright[(hpiece, vpiece)] = unichr(9564) _mergeleft[(hpiece, vpiece)] = unichr(9567) _mergeright[(hpiece, vpiece)] = unichr(9570) _mergetop[(hpiece, vpiece)] = unichr(9573) _mergebottom[(hpiece, vpiece)] = unichr(9576) _mergecenter[(hpiece, vpiece)] = unichr(9579) for hpiece in _pieces_light_horizontal: for vpiece in _pieces_double_vertical: _mergetopleft[(hpiece, vpiece)] = unichr(9554) _mergetopright[(hpiece, vpiece)] = unichr(9557) _mergebottomleft[(hpiece, vpiece)] = unichr(9560) _mergebottomright[(hpiece, vpiece)] = unichr(9563) _mergeleft[(hpiece, vpiece)] = unichr(9566) _mergeright[(hpiece, vpiece)] = unichr(9569) _mergetop[(hpiece, vpiece)] = unichr(9572) _mergebottom[(hpiece, vpiece)] = unichr(9575) _mergecenter[(hpiece, vpiece)] = unichr(9578) def _merge(self, hsep, vsep, pos='center'): if not hsep: return '' if not vsep: return '' if hsep == ' ': return vsep if vsep == ' ': return hsep mergedict = getattr(self, '_merge'+pos) return mergedict.get((hsep, vsep), ' ')
[docs] def make(self, titles=False, hb='', vb='', padding=0, hp=0, vp=0, autoalign=True, rowtitles=False, coltitles=False, hc=' ', vc='', ht=' ', vt='', border='', borderleft='', borderright='', bordertop='', borderbottom='', paddingleft=0, paddingright=0, paddingtop=0, paddingbottom=0, width=None, box=True): """Create table representation. The following formatting options exist: *rowtitles*: Show row titles. Default: False *coltitles*: Show column titles. Default: False *hc*: Horizontal cell delimiter. Default: ' ' *vc*: Vertical cell delimiter. Default: '' *ht*: Horizontal delimiter between row titles and data cells. Default: ' ' *vt*: Vertical delimiter between column titles and data cells. Default: '' *borderleft*: Character used as the left table border. Default: '' *borderright*: Character used as the right table border. Default: '' *bordertop*: Character used as the top table border. Default: '' *borderbottom*: Character used as the bottom table border. Default: '' *paddingleft*: Left cell padding (number of space characters). Default: 0 *paddingright*: Right cell padding (number of space characters). Default: 0 *paddingtop*: Top cell padding (number of space characters). Default: 0 *paddingbottom*: Bottom cell padding (number of space characters). Default: 0 *autoalign*: If *True*, infer alignment of columns from the column data. only for those columns for which no alignment has been set. Default: *True* *width*: If not *None*, set the width of the table. The rest of the characters will be cut from each line. Should be set to the terminal width for wide tables. Default: *None* *box*: If *True*, interpret certain characters in the options *hc*, *vc*, *ht*, *vt*, *borderleft*, *borderright*, *bordertop*, *borderbottom*, *hb* and *vb* as unicode box drawing characters (0x2500..0x2580). May not be available on all systems. See the section "special delimiters" for a list of characters that are interpreted. Default: *True* The following shortcuts exist: *titles*: Show row and column titles. Overrides *rowtitles* and *coltitles*. Default: False *hb*: Set horizontal borders. Overrides *bordertop* and *borderbottom*. Default: '' *vb*: Set vertical borders. Overrides *borderleft* and *borderright*. Default: '' *padding*: Set cell padding (number of space characters). Overrides *paddingtop*, *paddingbottom*, *paddingleft* and *paddingright*. Default: 0 *hp*: Set horizontal cell padding (number of space characters). Overrides *paddingleft* and *paddingright*. Default: 0 *vp*: Set vertical cell padding (number of space characters). Overrides *paddingtop* and *paddingbottom*. Default: 0 *border*: Set borders. Overrides *borderleft*, *borderright*, *bordertop* and *borderbottom*. Default: '' Special delimiters: As long as *box* is *True* (default), certain characters in the options *hc*, *vc*, *ht*, *vt*, *borderleft*, *borderright*, *bordertop*, *borderbottom*, *hb* and *vb* are interpreted as unicode box drawing characters (0x2500..0x2580). May not be available on all systems. Further information can be found at - http://en.wikipedia.org/wiki/Box-drawing_character - http://unicode.org/charts/PDF/U2500.pdf The following characters are interpreted: "l": light single line "h": heavy single line "d": double line "2": light double dash "u": heavy double dash "3": light triple dash "t": heavy triple dash "4": light quadruple dash "q": heavy quadruple dash """ # # to do: # - enable prettyprinting using box-drawing unicode characters # - support alignment for complex numbers # - support scientific number format # - support alignment for scientific number format # if titles: rowtitles = True coltitles = True if hb: bordertop = hb borderbottom = hb if vb: borderleft = vb borderright = vb if vp: paddingtop = vp paddingbottom = vp if hp: paddingleft = hp paddingright = hp if padding: paddingleft = padding paddingright = padding paddingtop = padding paddingbottom = padding if border: borderleft = border borderright = border bordertop = border borderbottom = border if not isinstance(hc, basestring) or len(hc) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(vc, basestring) or len(vc) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(ht, basestring) or len(ht) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(vt, basestring) or len(vt) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(borderleft, basestring) or len(borderleft) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(borderright, basestring) or len(borderright) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(bordertop, basestring) or len(bordertop) > 1: raise ValueError, 'must be a string no longer than one character' if not isinstance(borderbottom, basestring) or len(borderbottom) > 1: raise ValueError, 'must be a string no longer than one character' # interpret shortcuts for unicode box drawing characters _boxchars_horizontal = {'l': unichr(9472), 'h': unichr(9473), 'd': unichr(9552), '2': unichr(9548), 'u': unichr(9549), '3': unichr(9476), 't': unichr(9477), '4': unichr(9480), 'q': unichr(9481)} _boxchars_vertical = {'l': unichr(9474), 'h': unichr(9475), 'd': unichr(9553), '2': unichr(9550), 'u': unichr(9551), '3': unichr(9478), 't': unichr(9479), '4': unichr(9482), 'q': unichr(9483)} hc = _boxchars_vertical.get(str(hc), hc) ht = _boxchars_vertical.get(str(ht), ht) vc = _boxchars_horizontal.get(str(vc), vc) vt = _boxchars_horizontal.get(str(vt), vt) borderleft = _boxchars_vertical.get(str(borderleft), borderleft) borderright = _boxchars_vertical.get(str(borderright), borderright) bordertop = _boxchars_horizontal.get(str(bordertop), bordertop) borderbottom = _boxchars_horizontal.get(str(borderbottom), borderbottom) startcol = self.left() if startcol is None: return '' endcol = self.right() if endcol is None: return '' startrow = self.top() if startrow is None: return '' endrow = self.bottom() if endrow is None: return '' rowtitlescolwidth = self.rowtitlescolwidth() # automatically infer alignment from data colalign = self.colalign if autoalign: for colind in xrange(startcol, endcol+1): if colind in colalign: continue cells = self.column(colind) data = [cell.data for cell in cells] if all_of_type(data, float): colalign[colind] = 'point' elif all_of_type(data, (int, float, long, complex)): colalign[colind] = 'right' # create matrix of table cells allcolstrings = [self.colstrings(colind, withtitle=coltitles, colalign=colalign) for colind in xrange(startcol, endcol+1)] # change datastructure to represent a list of rows instead of a list of # columns allrowstrings = [] for r in xrange(len(allcolstrings[0])): rowstrings = [] for c in xrange(len(allcolstrings)): rowstrings.append(allcolstrings[c][r]) allrowstrings.append(rowstrings) # start constructing the table rows = [] # top border if bordertop: row = self._bordertopstring(allrowstrings, borderleft, borderright, bordertop, rowtitles, paddingleft, paddingright, hc, ht) rows.append(row) # column titles colwidths = self.colwidths(withtitle=coltitles, colalign=colalign) if coltitles: for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) row = self._coltitlesrowstring(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht) rows.append(row) for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) if vt: row = self._coltitlessepstring(allrowstrings, rowtitles, borderleft, borderright, paddingleft, paddingright, vt, hc, ht) rows.append(row) # main rows (first) if len(allrowstrings) > 0: rowtitle = self.rowtitles.get(self.top(), ' '*rowtitlescolwidth) if not rowtitle: rowtitle = ' '*rowtitlescolwidth for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) row = self._datarowstring(allrowstrings[0], rowtitle, borderleft, borderright, rowtitles, paddingleft, paddingright, hc, ht) rows.append(row) for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) # main rows (remaining) for rowind, rowstrings in enumerate(allrowstrings[1:], 1): rowtitle = self.rowtitles.get(rowind, ' '*rowtitlescolwidth) if vc: row = self._datasepstring(rowstrings, rowtitles, borderleft, borderright, paddingleft, paddingright, vc, hc, ht) rows.append(row) for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) row = self._datarowstring(rowstrings, rowtitle, borderleft, borderright, rowtitles, paddingleft, paddingright, hc, ht) rows.append(row) for i in xrange(paddingtop): row = self._padrow(rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths) rows.append(row) # bottom border if borderbottom: row = self._borderbottomstring(allrowstrings, borderleft, borderright, borderbottom, rowtitles, paddingleft, paddingright, hc, ht) rows.append(row) # cut rows according to terminal width if width is not None: for i in xrange(len(rows)): rows[i] = rows[i][:width] # return complete string representation of the table return '\n'.join(rows)
[docs] def dimensions(self, **kwargs): """Get table dimensions (the space needed to print the table) in the form (number of terminal rows, number of terminal columns). All keyword arguments are passed to :py:meth:`Table.make`.""" strrep = self.make(**kwargs) if not strrep: return 0, 0 lines = strrep.split('\n') return len(lines), len(lines[0]) if lines else 0
def _coltitlessepstring(self, allrowstrings, rowtitles, borderleft, borderright, paddingleft, paddingright, vt, hc, ht): row = '' # left if borderleft: row += self._merge(borderleft, vt, 'left') if rowtitles: row += vt*paddingleft row += vt*self.rowtitlescolwidth() row += vt*paddingright row += self._merge(ht, vt, 'center') # main parts = [] for rowstring in allrowstrings[0]: part = '' part += vt*paddingleft part += vt*len(rowstring) part += vt*paddingright parts.append(part) sep = self._merge(hc, vt, 'center') row += sep.join(parts) # right if borderright: row += self._merge(borderright, vt, 'right') return row def _datasepstring(self, rowstrings, rowtitles, borderleft, borderright, paddingleft, paddingright, vc, hc, ht): row = '' # left if borderleft: row += self._merge(borderleft, vc, 'left') if rowtitles: row += vc*paddingleft row += vc*self.rowtitlescolwidth() row += vc*paddingright row += self._merge(ht, vc, 'center') # main parts = [] for rowstring in rowstrings: part = '' part += vc*paddingleft part += vc*len(rowstring) part += vc*paddingright parts.append(part) sep = self._merge(hc, vc, 'center') row += sep.join(parts) # right if borderright: row += self._merge(borderright, vc, 'right') return row def _padrow(self, rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht, colwidths): row = '' # left if borderleft: row += borderleft if rowtitles: row += ' '*paddingleft row += ' '*self.rowtitlescolwidth() row += ' '*paddingright row += ht # main parts = [] for colwidth in colwidths: part = '' part += ' '*paddingleft part += ' '*colwidth part += ' '*paddingright parts.append(part) row += hc.join(parts) # right if borderright: row += borderright return row def _coltitlesrowstring(self, rowtitles, borderleft, borderright, paddingleft, paddingright, hc, ht): row = '' # left if borderleft: row += borderleft if rowtitles: row += ' '*paddingleft row += ' '*self.rowtitlescolwidth() row += ' '*paddingright row += ht # main parts = [] for coltitle in self.coltitlestrings(): part = '' part += ' '*paddingleft part += str(coltitle) part += ' '*paddingright parts.append(part) row += hc.join(parts) # right if borderright: row += borderright return row def _bordertopstring(self, allrowstrings, borderleft, borderright, bordertop, rowtitles, paddingleft, paddingright, hc, ht): row = '' # left if borderleft: row += self._merge(borderleft, bordertop, 'topleft') if rowtitles: row += bordertop*paddingleft row += bordertop*self.rowtitlescolwidth() row += bordertop*paddingright row += self._merge(ht, bordertop, 'top') # main parts = [] for rowstring in allrowstrings[0]: part = '' part += bordertop*paddingleft part += bordertop*len(rowstring) part += bordertop*paddingright parts.append(part) sep = self._merge(hc, bordertop, 'top') row += sep.join(parts) # right if borderright: row += self._merge(borderright, bordertop, 'topright') return row def _borderbottomstring(self, allrowstrings, borderleft, borderright, borderbottom, rowtitles, paddingleft, paddingright, hc, ht): row = '' # left if borderleft: row += self._merge(borderleft, borderbottom, 'bottomleft') if rowtitles: row += borderbottom*paddingleft row += borderbottom*self.rowtitlescolwidth() row += borderbottom*paddingright row += self._merge(ht, borderbottom, 'bottom') # main parts = [] for rowstring in allrowstrings[0]: part = '' part += borderbottom*paddingleft part += borderbottom*len(rowstring) part += borderbottom*paddingright parts.append(part) sep = self._merge(hc, borderbottom, 'bottom') row += sep.join(parts) # right if borderright: row += self._merge(borderright, borderbottom, 'bottomright') return row def _datarowstring(self, rowstrings, rowtitle, borderleft, borderright, rowtitles, paddingleft, paddingright, hc, ht): row = '' # left if borderleft: row += borderleft if rowtitles: row += ' '*paddingleft row += str(rowtitle) row += ' '*paddingright row += ht # main parts = [] for rowstring in rowstrings: part = '' part += ' '*paddingleft part += rowstring part += ' '*paddingright parts.append(part) row += hc.join(parts) # right if borderright: row += borderright return row
[docs] def rowtitlescolwidth(self): return max(len(str(rowtitle)) for rowtitle in self.rowtitles.values()) \ if self.rowtitles else 0
[docs] def rowtitlescolstrings(self): start = self.top() end = self.bottom() out = [] rowtitlescolwidth = self.rowtitlescolwidth() for rowind in xrange(start, end+1): if not rowind in self.rowtitles: out.append(' '*rowtitlescolwidth) continue rowtitle = str(self.rowtitles[rowind]) out.append(rowtitle.ljust(rowtitlescolwidth)) return out
[docs] def coltitlestrings(self, colalign=None): start = self.left() end = self.right() out = [] for colind in xrange(start, end+1): colwidth = self.colwidth(colind, withtitle=True, colalign=colalign) if not colind in self.coltitles: out.append(' '*colwidth) continue coltitle = str(self.coltitles[colind]) out.append(coltitle.ljust(colwidth)) return out
[docs] def colstrings(self, index, withtitle=False, colalign=None): out = [] if colalign is None: colalign is self.colalign align = colalign.get(index, DEFAULT_ALIGN) start = self.top() end = self.bottom() colwidth = self.colwidth(index, withtitle=withtitle, colalign=colalign) if align == 'point': colwidth_before = self.colwidth_before_point(index) colwidth_after = self.colwidth_after_point(index) for rowind in xrange(start, end+1): if (rowind, index) not in self.cells: out.append(' '*colwidth) continue cell = self.cells[rowind, index] before, after = cell.splitpoint() before = before.rjust(colwidth_before) after = after.ljust(colwidth_after) point = '.' if '.' in str(cell) else ' ' out.append(before + point + after) elif align == 'left': for rowind in xrange(start, end+1): if (rowind, index) not in self.cells: out.append(' '*colwidth) continue cell = self.cells[rowind, index] out.append(str(cell).ljust(colwidth)) elif align == 'right': for rowind in xrange(start, end+1): if (rowind, index) not in self.cells: out.append(' '*colwidth) continue cell = self.cells[rowind, index] out.append(str(cell).rjust(colwidth)) elif align == 'center': for rowind in xrange(start, end+1): if (rowind, index) not in self.cells: out.append(' '*colwidth) continue cell = self.cells[rowind, index] space = (colwidth - len(cell)) first = len(cell) + space/2 out.append(str(cell).ljust(first).rjust(colwidth)) return out
[docs] def colwidth_before_point(self, index): cells = self.column(index) return max(cell.width_before_point() for cell in cells) if cells else 0
[docs] def colwidth_after_point(self, index): cells = self.column(index) return max(cell.width_after_point() for cell in cells) if cells else 0
[docs] def colwidth(self, index, withtitle=False, colalign=None): cells = self.column(index) if colalign is None: colalign = self.colalign align = colalign.get(index, DEFAULT_ALIGN) if align == 'point': maxcellwidth = self.colwidth_before_point(index) + 1 \ + self.colwidth_after_point(index) else: maxcellwidth = max(cell.width() for cell in cells) if cells else 0 if withtitle and index in self.coltitles \ and len(str(self.coltitles[index])) > maxcellwidth: maxcellwidth = len(str(self.coltitles[index])) return maxcellwidth
[docs] def colwidths(self, withtitle=False, colalign=None): start = self.left() end = self.right() out = [] for colind in xrange(start, end+1): out.append(self.colwidth(colind, withtitle=withtitle, colalign=colalign)) return out
[docs]class Cell(object): _instance_count = 0 def __init__(self, table, (row, column), data=None): self._data = data self._table = table self.table.cells[(row, column)] = self self._instance_id = self._instance_count self.__class__._instance_count += 1 @property def data(self): return self._data @data.setter
[docs] def data(self, data): self._data = data
@property def table(self): return self._table @table.setter
[docs] def table(self, t): if not isinstance(t, (Table, type(None))): raise TypeError('must be of type Table or None') try: inds = self._table.cells.index(self) del self._table.cells[inds] except: pass self._table = t if t is not None: t.cells[inds] = self
@property
[docs] def instance_count(self): return self.__class__._instance_count
@property
[docs] def instance_id(self): return self._instance_id
[docs] def inds(self): return self.table.cells.index(self)
[docs] def rowind(self): return self.table.cells.index(self)[0]
[docs] def colind(self): return self.table.cells.index(self)[1]
[docs] def column(self): return self.table.column(self.colind())
[docs] def row(self): return self.table.row(self.rowind())
[docs] def align(self): return self.table.colalign.get(self.colind(), DEFAULT_ALIGN)
def __str__(self): return str(self.data).strip() def __len__(self): return len(str(self))
[docs] def width(self): return len(self)
[docs] def splitpoint(self): parts = str(self).split('.', 1) if len(parts) < 2: parts.append('') return parts
[docs] def width_before_point(self): return len(self.splitpoint()[0])
[docs] def width_after_point(self): return len(self.splitpoint()[1])
[docs] def width_before_and_after_point(self): parts = self.splitpoint() return len(parts[0]), len(parts[1])
def __repr__(self): return '<Cell%i>' % self.instance_id