2025-02-07 00:47:33 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""A generic tabular data output formatter interface."""
|
|
|
|
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
from collections import namedtuple
|
|
|
|
|
2025-02-07 00:48:48 +01:00
|
|
|
from cli_helpers.compat import (
|
|
|
|
text_type,
|
|
|
|
binary_type,
|
|
|
|
int_types,
|
|
|
|
float_types,
|
|
|
|
zip_longest,
|
|
|
|
)
|
2025-02-07 00:47:33 +01:00
|
|
|
from cli_helpers.utils import unique_items
|
2025-02-07 00:48:48 +01:00
|
|
|
from . import (
|
|
|
|
delimited_output_adapter,
|
|
|
|
vertical_table_adapter,
|
|
|
|
tabulate_adapter,
|
|
|
|
tsv_output_adapter,
|
|
|
|
)
|
2025-02-07 00:47:33 +01:00
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
import itertools
|
|
|
|
|
2025-02-07 00:48:48 +01:00
|
|
|
MISSING_VALUE = "<null>"
|
2025-02-07 00:47:33 +01:00
|
|
|
MAX_FIELD_WIDTH = 500
|
|
|
|
|
|
|
|
TYPES = {
|
|
|
|
type(None): 0,
|
|
|
|
bool: 1,
|
|
|
|
int: 2,
|
|
|
|
float: 3,
|
|
|
|
Decimal: 3,
|
|
|
|
binary_type: 4,
|
2025-02-07 00:48:48 +01:00
|
|
|
text_type: 5,
|
2025-02-07 00:47:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
OutputFormatHandler = namedtuple(
|
2025-02-07 00:48:48 +01:00
|
|
|
"OutputFormatHandler", "format_name preprocessors formatter formatter_args"
|
|
|
|
)
|
2025-02-07 00:47:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
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:
|
2025-02-07 00:48:48 +01:00
|
|
|
raise ValueError('unrecognized format_name "{}"'.format(format_name))
|
2025-02-07 00:47:33 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def supported_formats(self):
|
|
|
|
"""The names of the supported output formats in a :class:`tuple`."""
|
|
|
|
return tuple(self._output_formats.keys())
|
|
|
|
|
|
|
|
@classmethod
|
2025-02-07 00:48:48 +01:00
|
|
|
def register_new_formatter(
|
|
|
|
cls, format_name, handler, preprocessors=(), kwargs=None
|
|
|
|
):
|
2025-02-07 00:47:33 +01:00
|
|
|
"""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(
|
2025-02-07 00:48:48 +01:00
|
|
|
format_name, preprocessors, handler, kwargs or {}
|
|
|
|
)
|
|
|
|
|
|
|
|
def format_output(
|
|
|
|
self,
|
|
|
|
data,
|
|
|
|
headers,
|
|
|
|
format_name=None,
|
|
|
|
preprocessors=(),
|
|
|
|
column_types=None,
|
|
|
|
**kwargs
|
|
|
|
):
|
2025-02-07 00:47:33 +01:00
|
|
|
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))
|
|
|
|
|
2025-02-07 00:48:48 +01:00
|
|
|
(_, _preprocessors, formatter, fkwargs) = self._output_formats[format_name]
|
2025-02-07 00:47:33 +01:00
|
|
|
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):
|
2025-02-07 00:48:48 +01:00
|
|
|
data, headers = f(data, headers, column_types=column_types, **fkwargs)
|
2025-02-07 00:47:33 +01:00
|
|
|
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(
|
2025-02-07 00:48:48 +01:00
|
|
|
vertical_format,
|
|
|
|
vertical_table_adapter.adapter,
|
2025-02-07 00:47:33 +01:00
|
|
|
vertical_table_adapter.preprocessors,
|
2025-02-07 00:48:48 +01:00
|
|
|
{
|
|
|
|
"table_format": vertical_format,
|
|
|
|
"missing_value": MISSING_VALUE,
|
|
|
|
"max_field_width": None,
|
|
|
|
},
|
|
|
|
)
|
2025-02-07 00:47:33 +01:00
|
|
|
|
|
|
|
for delimited_format in delimited_output_adapter.supported_formats:
|
|
|
|
TabularOutputFormatter.register_new_formatter(
|
2025-02-07 00:48:48 +01:00
|
|
|
delimited_format,
|
|
|
|
delimited_output_adapter.adapter,
|
2025-02-07 00:47:33 +01:00
|
|
|
delimited_output_adapter.preprocessors,
|
2025-02-07 00:48:48 +01:00
|
|
|
{
|
|
|
|
"table_format": delimited_format,
|
|
|
|
"missing_value": "",
|
|
|
|
"max_field_width": None,
|
|
|
|
},
|
|
|
|
)
|
2025-02-07 00:47:33 +01:00
|
|
|
|
|
|
|
for tabulate_format in tabulate_adapter.supported_formats:
|
|
|
|
TabularOutputFormatter.register_new_formatter(
|
2025-02-07 00:48:48 +01:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
),
|
2025-02-07 00:47:33 +01:00
|
|
|
|
|
|
|
for tsv_format in tsv_output_adapter.supported_formats:
|
|
|
|
TabularOutputFormatter.register_new_formatter(
|
2025-02-07 00:48:48 +01:00
|
|
|
tsv_format,
|
|
|
|
tsv_output_adapter.adapter,
|
2025-02-07 00:47:33 +01:00
|
|
|
tsv_output_adapter.preprocessors,
|
2025-02-07 00:48:48 +01:00
|
|
|
{"table_format": tsv_format, "missing_value": "", "max_field_width": None},
|
|
|
|
)
|