# HG changeset patch # User Goffi # Date 1499018961 -7200 # Node ID 276e546b761988a913db9433e83a157ffa36b675 # Parent ad2a8e8b52daf76f2d372271ee64c8c4cea5807b jp (common): new ansi_ljust, ansi_rjust and ansi_center command + table: - ansi_ljust, ansi_rjust and ansi_center are equivalent to string.ljust, rjust and center, but handle strings with ANSI escape codes - new Table class to handle tabular data and display them with ASCII borders/separations. - Table is quite flexible, filters can be applied to data, header can be shown or not, columns/headers can be left/right aligned. - table can be displayed without decoration to align text data, using display_blank helping method diff -r ad2a8e8b52da -r 276e546b7619 frontends/src/jp/common.py --- a/frontends/src/jp/common.py Sun Jul 02 19:53:44 2017 +0200 +++ b/frontends/src/jp/common.py Sun Jul 02 20:09:21 2017 +0200 @@ -19,8 +19,10 @@ from sat_frontends.jp.constants import Const as C from sat.core.i18n import _ +from sat.core import exceptions from sat.tools.common import regex from sat.tools.common import uri +from sat.tools.common.ansi import ANSI as A from sat.tools import config from ConfigParser import NoSectionError, NoOptionError import json @@ -48,6 +50,26 @@ METADATA_SUFF = '_metadata.json' +def ansi_ljust(s, width): + """ljust method handling ANSI escape codes""" + cleaned = regex.ansiRemove(s) + return s + u' ' * (width - len(cleaned)) + + +def ansi_center(s, width): + """ljust method handling ANSI escape codes""" + cleaned = regex.ansiRemove(s) + diff = width - len(cleaned) + half = diff/2 + return half * u' ' + s + (half + diff % 2) * u' ' + + +def ansi_rjust(s, width): + """ljust method handling ANSI escape codes""" + cleaned = regex.ansiRemove(s) + return u' ' * (width - len(cleaned)) + s + + def getTmpDir(sat_conf, cat_dir, sub_dir=None): """Return directory used to store temporary files @@ -474,3 +496,219 @@ return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata else: return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj + + +class Table(object): + + def __init__(self, host, data, headers=None, filters=None): + """ + @param data(list[list]): table data + all lines must have the same number of columns + @param headers(iterable[unicode], None): names/titles of the columns + if not None, must have same number of columns as data + @param filters(iterable[(callable, unicode)], None): values filters + the callable will get col value as argument and must return a string + if it's unicode, it will be used with .format and must countain u'{}' which will be replaced with the string + if not None, must have same number of columns as data + """ + self.host = host + # headers are columns names/titles, can be None + self.headers = headers + # sizes fof columns without headers, + # headers may be larger + self.sizes = [] + # rows countains one list per row with columns values + self.rows = [] + + size = None + for line in data: + new_row = [] + for idx, value in enumerate(line): + if filters is not None and filters[idx] is not None: + filter_ = filters[idx] + if isinstance(filter_, basestring): + col_value = filter_.format(value) + else: + col_value = filter_(value) + # we count size without ANSI code as they will change length of the string + # when it's mostly style/color changes. + col_size = len(regex.ansiRemove(col_value)) + else: + col_value = unicode(value) + col_size = len(col_value) + new_row.append(col_value) + if size is None: + self.sizes.append(col_size) + else: + self.sizes[idx] = max(self.sizes[idx], col_size) + if size is None: + size = len(new_row) + if headers is not None and len(headers) != size: + raise exceptions.DataError(u'headers size is not coherent with rows') + else: + if len(new_row) != size: + raise exceptions.DataError(u'rows size is not coherent') + self.rows.append(new_row) + + @staticmethod + def readDictValues(data, keys, defaults=None): + if defaults is None: + defaults = {} + for key in keys: + try: + yield data[key] + except KeyError as e: + default = defaults.get(key) + if default is not None: + yield default + else: + raise e + + @classmethod + def fromDict(cls, host, data, keys=None, headers=None, filters=None, defaults=None): + """Prepare a table to display it + + the whole data will be read and kept into memory, + to be printed + @param data(list[dict[unicode, unicode]]): data to create the table from + @param keys(iterable[unicode], None): keys to get + if None, all keys will be used + @param headers(iterable[unicode], None): name of the columns + names must be in same order as keys + @param filters(dict[unicode, (callable,unicode)), None): filter to use on values + keys correspond to keys to filter, and value is a callable or unicode which + will get the value as argument and must return a string + @param defaults(dict[unicode, unicode]): default value to use + if None, an exception will be raised if not value is found + """ + if keys is None and headers is not None: + # FIXME: keys are not needed with OrderedDict, + raise exceptions.DataError(u'You must specify keys order to used headers') + if keys is None: + keys = data[0].keys() + if headers is None: + headers = keys + filters = [filters.get(k) for k in keys] + return cls(host, (cls.readDictValues(d, keys, defaults) for d in data), headers, filters) + + def _headers(self, head_sep, alignment=u'left', style=None): + """Render headers + + @param head_sep(unicode): sequence to use as separator + @param alignment(unicode): how to align, can be left, center or right + @param style(unicode, iterable[unicode], None): ANSI escape sequences to apply + """ + headers = [] + if isinstance(style, basestring): + style = [style] + for idx, header in enumerate(self.headers): + size = self.sizes[idx] + if alignment == u'left': + rendered = header[:size].ljust(size) + elif alignment == u'center': + rendered = header[:size].center(size) + elif alignment == u'right': + rendered = header[:size].rjust(size) + else: + raise exceptions.InternalError(u'bad alignment argument') + if style: + args = style + [rendered] + rendered = A.color(*args) + headers.append(rendered) + return head_sep.join(headers) + + def display(self, + head_alignment = u'left', + columns_alignment = u'left', + head_style = None, + show_header=True, + show_borders=True, + col_sep=u' │ ', + top_left=u'┌', + top=u'─', + top_sep=u'─┬─', + top_right=u'┐', + left=u'│', + right=None, + head_sep=None, + head_line=u'┄', + head_line_left=u'├', + head_line_sep=u'┄┼┄', + head_line_right=u'┤', + bottom_left=u'└', + bottom=None, + bottom_sep=u'─┴─', + bottom_right=u'┘' + ): + """Print the table + + @param show_header(bool): True if header need no be shown + @param show_borders(bool): True if borders need no be shown + @param head_alignment(unicode): how to align headers, can be left, center or right + @param columns_alignment(unicode): how to align columns, can be left, center or right + @param col_sep(unicode): separator betweens columns + @param head_line(unicode): character to use to make line under head + """ + col_sep_size = len(regex.ansiRemove(col_sep)) + if right is None: + right = left + if top_sep is None: + top_sep = col_sep_size * top + if head_sep is None: + head_sep = col_sep + if bottom is None: + bottom = top + if bottom_sep is None: + bottom_sep = col_sep_size * bottom + if not show_borders: + left = right = head_line_left = head_line_right = u'' + # top border + if show_borders: + self.host.disp( + top_left + + top_sep.join([top*size for size in self.sizes]) + + top_right + ) + + # headers + if show_header: + self.host.disp( + left + + self._headers(head_sep, head_alignment, head_style) + + right + ) + # header line + self.host.disp( + head_line_left + + head_line_sep.join([head_line*size for size in self.sizes]) + + head_line_right + ) + + # content + if columns_alignment == u'left': + alignment = lambda idx, s: ansi_ljust(s, self.sizes[idx]) + elif columns_alignment == u'center': + alignment = lambda idx, s: ansi_center(s, self.sizes[idx]) + elif columns_alignment == u'right': + alignment = lambda idx, s: ansi_rjust(s, self.sizes[idx]) + else: + raise exceptions.InternalError(u'bad columns alignment argument') + + for row in self.rows: + self.host.disp(left + col_sep.join([alignment(idx,c) for idx,c in enumerate(row)]) + right) + + if show_borders: + # bottom border + self.host.disp( + bottom_left + + bottom_sep.join([bottom*size for size in self.sizes]) + + bottom_right + ) + + def display_blank(self, **kwargs): + """Display table without visible borders""" + kwargs_ = {'col_sep':u' ', 'head_line_sep':u' ', 'show_borders':False} + kwargs_.update(kwargs) + self.display(self, + **kwargs_ + )