From c9ef4ed560a5de28fce031882257b6741565284e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 04:59:59 +0200 Subject: Adding upstream version 2.3.1. Signed-off-by: Daniel Baumann --- cli_helpers/tabular_output/output_formatter.py | 255 +++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 cli_helpers/tabular_output/output_formatter.py (limited to 'cli_helpers/tabular_output/output_formatter.py') diff --git a/cli_helpers/tabular_output/output_formatter.py b/cli_helpers/tabular_output/output_formatter.py new file mode 100644 index 0000000..6cadf6c --- /dev/null +++ b/cli_helpers/tabular_output/output_formatter.py @@ -0,0 +1,255 @@ +# -*- 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, + 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.get_preprocessors(tabulate_format), + { + "table_format": tabulate_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}, + ) -- cgit v1.2.3