From 8e32fd57a9fff7765053d5705b82762d686fdb77 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Tue, 26 Jun 2018 17:51:03 -0400 Subject: [PATCH 1/3] Tweaked so that object formatter and display formatter functions can be chained together. This allows an value generated by an object formatter to be passed to an existing display formatter function. For example, this allows a computed float value to from an object formatter to go through a display formatter to round/truncate digits. --- tableformatter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tableformatter.py b/tableformatter.py index d427012..52ca23b 100755 --- a/tableformatter.py +++ b/tableformatter.py @@ -928,8 +928,8 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans formatter = self._get_column_option(column_index, TableFormatter.COL_OPT_FIELD_FORMATTER) obj_formatter = self._get_column_option(column_index, TableFormatter.COL_OPT_OBJECT_FORMATTER) if obj_formatter is not None and callable(obj_formatter): - field_string = obj_formatter(entry_obj) - elif formatter is not None and callable(formatter): + field_obj = obj_formatter(entry_obj) + if formatter is not None and callable(formatter): field_string = formatter(field_obj) elif isinstance(field_obj, str): field_string = field_obj @@ -959,8 +959,8 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans formatter = self._get_column_option(column_index, TableFormatter.COL_OPT_FIELD_FORMATTER) obj_formatter = self._get_column_option(column_index, TableFormatter.COL_OPT_OBJECT_FORMATTER) if obj_formatter is not None and callable(obj_formatter): - field_string = obj_formatter(entry) - elif formatter is not None and callable(formatter): + field = obj_formatter(entry) + if formatter is not None and callable(formatter): field_string = formatter(field, ) elif isinstance(field, str): field_string = field From a247b81efc70eaf65cb52b547238275fad52153c Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Thu, 28 Jun 2018 11:53:28 -0400 Subject: [PATCH 2/3] Added a row decorator option to tableformatter. Now allows a function to return the per-row options. Currently the only supported option is text color --- examples/formatters.py | 26 +++++++++++++++++-- tableformatter.py | 59 ++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/examples/formatters.py b/examples/formatters.py index 9a2f1c0..caa846a 100755 --- a/examples/formatters.py +++ b/examples/formatters.py @@ -4,6 +4,7 @@ Demonstration of field and object formatters for both object and tuple based table entries """ import tableformatter as tf +from typing import Tuple class MyRowObject(object): @@ -74,6 +75,13 @@ def int2word(num, separator="-"): print("num out of range") +def decorate_row_obj(row_obj: MyRowObject) -> dict: + """Example row decorator function processing row objects""" + opts = dict() + if row_obj.field4 % 4 == 0: + opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_GREEN + return opts + rows = [MyRowObject(None, None, 17, 4), MyRowObject('123', '123', 5, 56), MyRowObject(123, 123, 5, 56), @@ -91,7 +99,21 @@ def int2word(num, separator="-"): tf.Column('Multiplied', obj_formatter=multiply)) print("Formatters on object-based row entries") -print(tf.generate_table(rows, columns)) +print(tf.generate_table(rows, columns, row_decorator=decorate_row_obj)) + + +def decorate_row_tuples(row_tuple: Tuple) -> dict: + """ + Example decorator function processing row tuples/iterables + + Note that this is the tuple as provided to the TableFormatter prior + to display formatting performed on each column + """ + opts = dict() + if len(row_tuple) >= 4 and row_tuple[3] % 4 == 0: + opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_GREEN + return opts + rows = [(None, None, 17, 4, None), ('123', '123', 5, 56, None), @@ -108,4 +130,4 @@ def int2word(num, separator="-"): tf.Column('Multiplied', obj_formatter=multiply_tuple)) print("Formatters on tuple-based row entries") -print(tf.generate_table(rows, columns)) +print(tf.generate_table(rows, columns, row_decorator=decorate_row_tuples)) diff --git a/tableformatter.py b/tableformatter.py index 52ca23b..dc0c6a8 100755 --- a/tableformatter.py +++ b/tableformatter.py @@ -600,9 +600,21 @@ def set_default_grid(grid: Grid) -> None: DEFAULT_GRID = grid -def generate_table(rows: Iterable[Union[Iterable, object]], columns: Collection[Union[str, Tuple[str, dict]]]=None, - grid_style: Optional[Grid]=None, transpose: bool=False) -> str: - """Convenience function to easily generate a table from rows/columns""" +def generate_table(rows: Iterable[Union[Iterable, object]], + columns: Collection[Union[str, Tuple[str, dict]]]=None, + grid_style: Optional[Grid]=None, + transpose: bool=False, + row_decorator: Callable=None) -> str: + """ + Convenience function to easily generate a table from rows/columns + + :param rows: iterable of objects or iterable fields + :param columns: Iterable of column definitions + :param grid_style: The grid style to use + :param transpose: Transpose the rows/columns for display + :param row_decorator: decorator function to apply per-row options + :return: formatted string containing the table + """ show_headers = True use_attrib = False if isinstance(columns, Collection) and len(columns) > 0: @@ -634,7 +646,7 @@ def generate_table(rows: Iterable[Union[Iterable, object]], columns: Collection[ if grid_style is None: grid_style = DEFAULT_GRID formatter = TableFormatter(columns, grid_style=grid_style, show_header=show_headers, - use_attribs=use_attrib, transpose=transpose) + use_attribs=use_attrib, transpose=transpose, row_decorator=row_decorator) return formatter.generate_table(rows) @@ -696,7 +708,7 @@ def Column(col_name: str, return col_name, opts -def Row(*args, text_color: TableColors=None): +def Row(*args, text_color: Union[TableColors, str]=None): """ Processes row options and generates a tuple in the format the TableFormatter expects :param args: Can be either 1 object or a list of values @@ -750,7 +762,8 @@ def __init__(self, show_header=True, use_attribs=False, transpose=False, - row_show_header=False): + row_show_header=False, + row_decorator: Callable=None): """ :param columns: list of either column names or tuples of (column name, dict of column options) :param cell_padding: number of spaces to pad to the left/right of each column @@ -776,6 +789,7 @@ def __init__(self, TableFormatter.TABLE_OPT_TRANSPOSE: transpose, TableFormatter.TABLE_OPT_ROW_HEADER: row_show_header} self._show_header = show_header + self._row_decorator = row_decorator for col_index, column in enumerate(columns): if isinstance(column, tuple) and len(column) > 1 and isinstance(column[1], dict): @@ -911,11 +925,14 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans except TypeError: # not iterable, so we just use the object directly entry_obj = entry + if self._row_decorator is not None: + entry_opts = self._row_decorator(entry_obj) else: - - if len(entry) == 2: - entry_opts = entry[1] entry_obj = entry[0] + if self._row_decorator is not None: + entry_opts = self._row_decorator(entry_obj) + if len(entry) == 2 and isinstance(entry[1], dict): + entry_opts.update(entry[1]) for column_index, attrib_name in enumerate(self._column_attribs): field_obj = None @@ -949,9 +966,11 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans row.append(field_lines) else: - if len(entry) == len(self._columns) + 1: + if self._row_decorator is not None: + entry_opts = self._row_decorator(entry) + if len(entry) == len(self._columns) + 1 and isinstance(entry[len(self._columns)], dict): # if there is exactly 1 more entry than columns, the last one is metadata - entry_opts = entry[len(self._columns)] + entry_opts.update(entry[len(self._columns)]) for column_index, field in enumerate(entry): # skip extra values beyond the columns configured @@ -1202,6 +1221,10 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans out_string += self._grid_style.border_right_row_divider out_string += '\n' + opts = dict() + if row_index < len(row_opts): + opts = row_opts[row_index] + # loop for each line in the row num_lines = row_counts[row_index] for row_line in range(0, num_lines): @@ -1228,23 +1251,25 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans else: mapped_line = row_line - int((num_lines - len(field)) / 2) - opts = dict() if len(field) > mapped_line >= 0: - row_line_text = field[mapped_line] - if row_index < len(row_opts): - opts = row_opts[row_index] # if the field has a line for the current row line, add it + row_line_text = field[mapped_line] else: row_line_text = '' + + colorize = len(row_line_text.strip()) > 0 and TableFormatter.ROW_OPT_TEXT_COLOR in opts.keys() + row_line_text = _pad_columns(row_line_text, pad_char=self._grid_style.cell_pad_char, align=halign_cells[col_index], width=col_widths[col_index]) - if TableFormatter.ROW_OPT_TEXT_COLOR in opts.keys(): + if colorize: row_line_text = opts[TableFormatter.ROW_OPT_TEXT_COLOR] + row_line_text out_string += row_line_text - out_string += pad_string + TableColors.RESET + out_string += pad_string + if colorize: + out_string += TableColors.RESET if self._grid_style.border_right: out_string += self._grid_style.border_right_span(row_index) From e4da289b06c39d412ef2e2d0c318e2a2483962d6 Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Thu, 28 Jun 2018 17:43:04 -0400 Subject: [PATCH 3/3] renamed decorator to tagger --- examples/formatters.py | 13 +++++++------ tableformatter.py | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/examples/formatters.py b/examples/formatters.py index caa846a..b96d833 100755 --- a/examples/formatters.py +++ b/examples/formatters.py @@ -75,8 +75,9 @@ def int2word(num, separator="-"): print("num out of range") -def decorate_row_obj(row_obj: MyRowObject) -> dict: - """Example row decorator function processing row objects""" +def tag_row_obj(row_obj: MyRowObject) -> dict: + """Example row tagging function processing row objects and returning + optional properties""" opts = dict() if row_obj.field4 % 4 == 0: opts[tf.TableFormatter.ROW_OPT_TEXT_COLOR] = tf.TableColors.TEXT_COLOR_GREEN @@ -99,12 +100,12 @@ def decorate_row_obj(row_obj: MyRowObject) -> dict: tf.Column('Multiplied', obj_formatter=multiply)) print("Formatters on object-based row entries") -print(tf.generate_table(rows, columns, row_decorator=decorate_row_obj)) +print(tf.generate_table(rows, columns, row_tagger=tag_row_obj)) -def decorate_row_tuples(row_tuple: Tuple) -> dict: +def tag_row_tuples(row_tuple: Tuple) -> dict: """ - Example decorator function processing row tuples/iterables + Example row tagging function processing row tuples/iterables Note that this is the tuple as provided to the TableFormatter prior to display formatting performed on each column @@ -130,4 +131,4 @@ def decorate_row_tuples(row_tuple: Tuple) -> dict: tf.Column('Multiplied', obj_formatter=multiply_tuple)) print("Formatters on tuple-based row entries") -print(tf.generate_table(rows, columns, row_decorator=decorate_row_tuples)) +print(tf.generate_table(rows, columns, row_tagger=tag_row_tuples)) diff --git a/tableformatter.py b/tableformatter.py index dc0c6a8..dac99a0 100755 --- a/tableformatter.py +++ b/tableformatter.py @@ -604,7 +604,7 @@ def generate_table(rows: Iterable[Union[Iterable, object]], columns: Collection[Union[str, Tuple[str, dict]]]=None, grid_style: Optional[Grid]=None, transpose: bool=False, - row_decorator: Callable=None) -> str: + row_tagger: Callable=None) -> str: """ Convenience function to easily generate a table from rows/columns @@ -612,7 +612,7 @@ def generate_table(rows: Iterable[Union[Iterable, object]], :param columns: Iterable of column definitions :param grid_style: The grid style to use :param transpose: Transpose the rows/columns for display - :param row_decorator: decorator function to apply per-row options + :param row_tagger: decorator function to apply per-row options :return: formatted string containing the table """ show_headers = True @@ -646,7 +646,7 @@ def generate_table(rows: Iterable[Union[Iterable, object]], if grid_style is None: grid_style = DEFAULT_GRID formatter = TableFormatter(columns, grid_style=grid_style, show_header=show_headers, - use_attribs=use_attrib, transpose=transpose, row_decorator=row_decorator) + use_attribs=use_attrib, transpose=transpose, row_tagger=row_tagger) return formatter.generate_table(rows) @@ -763,7 +763,7 @@ def __init__(self, use_attribs=False, transpose=False, row_show_header=False, - row_decorator: Callable=None): + row_tagger: Callable=None): """ :param columns: list of either column names or tuples of (column name, dict of column options) :param cell_padding: number of spaces to pad to the left/right of each column @@ -789,7 +789,7 @@ def __init__(self, TableFormatter.TABLE_OPT_TRANSPOSE: transpose, TableFormatter.TABLE_OPT_ROW_HEADER: row_show_header} self._show_header = show_header - self._row_decorator = row_decorator + self._row_tagger = row_tagger for col_index, column in enumerate(columns): if isinstance(column, tuple) and len(column) > 1 and isinstance(column[1], dict): @@ -925,12 +925,12 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans except TypeError: # not iterable, so we just use the object directly entry_obj = entry - if self._row_decorator is not None: - entry_opts = self._row_decorator(entry_obj) + if self._row_tagger is not None: + entry_opts = self._row_tagger(entry_obj) else: entry_obj = entry[0] - if self._row_decorator is not None: - entry_opts = self._row_decorator(entry_obj) + if self._row_tagger is not None: + entry_opts = self._row_tagger(entry_obj) if len(entry) == 2 and isinstance(entry[1], dict): entry_opts.update(entry[1]) @@ -966,8 +966,8 @@ def generate_table(self, entries: Iterable[Union[Iterable, object]], force_trans row.append(field_lines) else: - if self._row_decorator is not None: - entry_opts = self._row_decorator(entry) + if self._row_tagger is not None: + entry_opts = self._row_tagger(entry) if len(entry) == len(self._columns) + 1 and isinstance(entry[len(self._columns)], dict): # if there is exactly 1 more entry than columns, the last one is metadata entry_opts.update(entry[len(self._columns)])