# -*- 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 = '' 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 `_ - `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})