1
0
Fork 0
cli-helpers/cli_helpers/tabular_output/output_formatter.py
Daniel Baumann d8a70e48ab
Adding upstream version 2.1.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-07 00:47:33 +01:00

228 lines
8.2 KiB
Python

# -*- 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})