summaryrefslogtreecommitdiffstats
path: root/cli_helpers/tabular_output/output_formatter.py
diff options
context:
space:
mode:
Diffstat (limited to 'cli_helpers/tabular_output/output_formatter.py')
-rw-r--r--cli_helpers/tabular_output/output_formatter.py228
1 files changed, 228 insertions, 0 deletions
diff --git a/cli_helpers/tabular_output/output_formatter.py b/cli_helpers/tabular_output/output_formatter.py
new file mode 100644
index 0000000..fce44b2
--- /dev/null
+++ b/cli_helpers/tabular_output/output_formatter.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+"""A generic tabular data output formatter interface."""
+
+from __future__ import unicode_literals
+from collections import namedtuple
+
+from cli_helpers.compat import (text_type, binary_type, int_types, float_types,
+ zip_longest)
+from cli_helpers.utils import unique_items
+from . import (delimited_output_adapter, vertical_table_adapter,
+ tabulate_adapter, terminaltables_adapter, tsv_output_adapter)
+from decimal import Decimal
+
+import itertools
+
+MISSING_VALUE = '<null>'
+MAX_FIELD_WIDTH = 500
+
+TYPES = {
+ type(None): 0,
+ bool: 1,
+ int: 2,
+ float: 3,
+ Decimal: 3,
+ binary_type: 4,
+ text_type: 5
+}
+
+OutputFormatHandler = namedtuple(
+ 'OutputFormatHandler',
+ 'format_name preprocessors formatter formatter_args')
+
+
+class TabularOutputFormatter(object):
+ """An interface to various tabular data formatting libraries.
+
+ The formatting libraries supported include:
+ - `tabulate <https://bitbucket.org/astanin/python-tabulate>`_
+ - `terminaltables <https://robpol86.github.io/terminaltables/>`_
+ - a CLI Helper vertical table layout
+ - delimited formats (CSV and TSV)
+
+ :param str format_name: An optional, default format name.
+
+ Usage::
+
+ >>> from cli_helpers.tabular_output import TabularOutputFormatter
+ >>> formatter = TabularOutputFormatter(format_name='simple')
+ >>> data = ((1, 87), (2, 80), (3, 79))
+ >>> headers = ('day', 'temperature')
+ >>> print(formatter.format_output(data, headers))
+ day temperature
+ ----- -------------
+ 1 87
+ 2 80
+ 3 79
+
+ You can use any :term:`iterable` for the data or headers::
+
+ >>> data = enumerate(('87', '80', '79'), 1)
+ >>> print(formatter.format_output(data, headers))
+ day temperature
+ ----- -------------
+ 1 87
+ 2 80
+ 3 79
+
+ """
+
+ _output_formats = {}
+
+ def __init__(self, format_name=None):
+ """Set the default *format_name*."""
+ self._format_name = None
+
+ if format_name:
+ self.format_name = format_name
+
+ @property
+ def format_name(self):
+ """The current format name.
+
+ This value must be in :data:`supported_formats`.
+
+ """
+ return self._format_name
+
+ @format_name.setter
+ def format_name(self, format_name):
+ """Set the default format name.
+
+ :param str format_name: The display format name.
+ :raises ValueError: if the format is not recognized.
+
+ """
+ if format_name in self.supported_formats:
+ self._format_name = format_name
+ else:
+ raise ValueError('unrecognized format_name "{}"'.format(
+ format_name))
+
+ @property
+ def supported_formats(self):
+ """The names of the supported output formats in a :class:`tuple`."""
+ return tuple(self._output_formats.keys())
+
+ @classmethod
+ def register_new_formatter(cls, format_name, handler, preprocessors=(),
+ kwargs=None):
+ """Register a new output formatter.
+
+ :param str format_name: The name of the format.
+ :param callable handler: The function that formats the data.
+ :param tuple preprocessors: The preprocessors to call before
+ formatting.
+ :param dict kwargs: Keys/values for keyword argument defaults.
+
+ """
+ cls._output_formats[format_name] = OutputFormatHandler(
+ format_name, preprocessors, handler, kwargs or {})
+
+ def format_output(self, data, headers, format_name=None,
+ preprocessors=(), column_types=None, **kwargs):
+ r"""Format the headers and data using a specific formatter.
+
+ *format_name* must be a supported formatter (see
+ :attr:`supported_formats`).
+
+ :param iterable data: An :term:`iterable` (e.g. list) of rows.
+ :param iterable headers: The column headers.
+ :param str format_name: The display format to use (optional, if the
+ :class:`TabularOutputFormatter` object has a default format set).
+ :param tuple preprocessors: Additional preprocessors to call before
+ any formatter preprocessors.
+ :param \*\*kwargs: Optional arguments for the formatter.
+ :return: The formatted data.
+ :rtype: str
+ :raises ValueError: If the *format_name* is not recognized.
+
+ """
+ format_name = format_name or self._format_name
+ if format_name not in self.supported_formats:
+ raise ValueError('unrecognized format "{}"'.format(format_name))
+
+ (_, _preprocessors, formatter,
+ fkwargs) = self._output_formats[format_name]
+ fkwargs.update(kwargs)
+ if column_types is None:
+ data = list(data)
+ column_types = self._get_column_types(data)
+ for f in unique_items(preprocessors + _preprocessors):
+ data, headers = f(data, headers, column_types=column_types,
+ **fkwargs)
+ return formatter(list(data), headers, column_types=column_types, **fkwargs)
+
+ def _get_column_types(self, data):
+ """Get a list of the data types for each column in *data*."""
+ columns = list(zip_longest(*data))
+ return [self._get_column_type(column) for column in columns]
+
+ def _get_column_type(self, column):
+ """Get the most generic data type for iterable *column*."""
+ type_values = [TYPES[self._get_type(v)] for v in column]
+ inverse_types = {v: k for k, v in TYPES.items()}
+ return inverse_types[max(type_values)]
+
+ def _get_type(self, value):
+ """Get the data type for *value*."""
+ if value is None:
+ return type(None)
+ elif type(value) in int_types:
+ return int
+ elif type(value) in float_types:
+ return float
+ elif isinstance(value, binary_type):
+ return binary_type
+ else:
+ return text_type
+
+
+def format_output(data, headers, format_name, **kwargs):
+ r"""Format output using *format_name*.
+
+ This is a wrapper around the :class:`TabularOutputFormatter` class.
+
+ :param iterable data: An :term:`iterable` (e.g. list) of rows.
+ :param iterable headers: The column headers.
+ :param str format_name: The display format to use.
+ :param \*\*kwargs: Optional arguments for the formatter.
+ :return: The formatted data.
+ :rtype: str
+
+ """
+ formatter = TabularOutputFormatter(format_name=format_name)
+ return formatter.format_output(data, headers, **kwargs)
+
+
+for vertical_format in vertical_table_adapter.supported_formats:
+ TabularOutputFormatter.register_new_formatter(
+ vertical_format, vertical_table_adapter.adapter,
+ vertical_table_adapter.preprocessors,
+ {'table_format': vertical_format, 'missing_value': MISSING_VALUE, 'max_field_width': None})
+
+for delimited_format in delimited_output_adapter.supported_formats:
+ TabularOutputFormatter.register_new_formatter(
+ delimited_format, delimited_output_adapter.adapter,
+ delimited_output_adapter.preprocessors,
+ {'table_format': delimited_format, 'missing_value': '', 'max_field_width': None})
+
+for tabulate_format in tabulate_adapter.supported_formats:
+ TabularOutputFormatter.register_new_formatter(
+ tabulate_format, tabulate_adapter.adapter,
+ tabulate_adapter.preprocessors +
+ (tabulate_adapter.style_output_table(tabulate_format),),
+ {'table_format': tabulate_format, 'missing_value': MISSING_VALUE, 'max_field_width': MAX_FIELD_WIDTH})
+
+for terminaltables_format in terminaltables_adapter.supported_formats:
+ TabularOutputFormatter.register_new_formatter(
+ terminaltables_format, terminaltables_adapter.adapter,
+ terminaltables_adapter.preprocessors +
+ (terminaltables_adapter.style_output_table(terminaltables_format),),
+ {'table_format': terminaltables_format, 'missing_value': MISSING_VALUE, 'max_field_width': MAX_FIELD_WIDTH})
+
+for tsv_format in tsv_output_adapter.supported_formats:
+ TabularOutputFormatter.register_new_formatter(
+ tsv_format, tsv_output_adapter.adapter,
+ tsv_output_adapter.preprocessors,
+ {'table_format': tsv_format, 'missing_value': '', 'max_field_width': None})