From 3fb37a1d0237869e8e37864d06c0dfd94bb43189 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 6 Sep 2021 06:12:56 +0200 Subject: Merging upstream version 2.2.0. Signed-off-by: Daniel Baumann --- cli_helpers/__init__.py | 2 +- cli_helpers/compat.py | 4 +- cli_helpers/config.py | 109 +++++++++++------ cli_helpers/tabular_output/__init__.py | 2 +- .../tabular_output/delimited_output_adapter.py | 28 +++-- cli_helpers/tabular_output/output_formatter.py | 101 ++++++++++------ cli_helpers/tabular_output/preprocessors.py | 111 ++++++++++++----- cli_helpers/tabular_output/tabulate_adapter.py | 131 ++++++++++++++++++--- .../tabular_output/terminaltables_adapter.py | 97 --------------- cli_helpers/tabular_output/tsv_output_adapter.py | 5 +- .../tabular_output/vertical_table_adapter.py | 19 +-- cli_helpers/utils.py | 34 ++++-- 12 files changed, 385 insertions(+), 258 deletions(-) delete mode 100644 cli_helpers/tabular_output/terminaltables_adapter.py (limited to 'cli_helpers') diff --git a/cli_helpers/__init__.py b/cli_helpers/__init__.py index a33997d..8a124bf 100644 --- a/cli_helpers/__init__.py +++ b/cli_helpers/__init__.py @@ -1 +1 @@ -__version__ = '2.1.0' +__version__ = "2.2.0" diff --git a/cli_helpers/compat.py b/cli_helpers/compat.py index 3f67c62..c938851 100644 --- a/cli_helpers/compat.py +++ b/cli_helpers/compat.py @@ -5,8 +5,8 @@ from decimal import Decimal import sys PY2 = sys.version_info[0] == 2 -WIN = sys.platform.startswith('win') -MAC = sys.platform == 'darwin' +WIN = sys.platform.startswith("win") +MAC = sys.platform == "darwin" if PY2: diff --git a/cli_helpers/config.py b/cli_helpers/config.py index 3d6cb16..7669717 100644 --- a/cli_helpers/config.py +++ b/cli_helpers/config.py @@ -16,11 +16,13 @@ logger = logging.getLogger(__name__) class ConfigError(Exception): """Base class for exceptions in this module.""" + pass class DefaultConfigValidationError(ConfigError): """Indicates the default config file did not validate correctly.""" + pass @@ -40,11 +42,19 @@ class Config(UserDict, object): file. """ - def __init__(self, app_name, app_author, filename, default=None, - validate=False, write_default=False, additional_dirs=()): + def __init__( + self, + app_name, + app_author, + filename, + default=None, + validate=False, + write_default=False, + additional_dirs=(), + ): super(Config, self).__init__() #: The :class:`ConfigObj` instance. - self.data = ConfigObj() + self.data = ConfigObj(encoding="utf8") self.default = {} self.default_file = self.default_config = None @@ -64,15 +74,19 @@ class Config(UserDict, object): elif default is not None: raise TypeError( '"default" must be a dict or {}, not {}'.format( - text_type.__name__, type(default))) + text_type.__name__, type(default) + ) + ) if self.write_default and not self.default_file: - raise ValueError('Cannot use "write_default" without specifying ' - 'a default file.') + raise ValueError( + 'Cannot use "write_default" without specifying ' "a default file." + ) if self.validate and not self.default_file: - raise ValueError('Cannot use "validate" without specifying a ' - 'default file.') + raise ValueError( + 'Cannot use "validate" without specifying a ' "default file." + ) def read_default_config(self): """Read the default config file. @@ -81,11 +95,18 @@ class Config(UserDict, object): the *default* file. """ if self.validate: - self.default_config = ConfigObj(configspec=self.default_file, - list_values=False, _inspec=True, - encoding='utf8') - valid = self.default_config.validate(Validator(), copy=True, - preserve_errors=True) + self.default_config = ConfigObj( + configspec=self.default_file, + list_values=False, + _inspec=True, + encoding="utf8", + ) + # ConfigObj does not set the encoding on the configspec. + self.default_config.configspec.encoding = "utf8" + + valid = self.default_config.validate( + Validator(), copy=True, preserve_errors=True + ) if valid is not True: for name, section in valid.items(): if section is True: @@ -93,8 +114,8 @@ class Config(UserDict, object): for key, value in section.items(): if isinstance(value, ValidateError): raise DefaultConfigValidationError( - 'section [{}], key "{}": {}'.format( - name, key, value)) + 'section [{}], key "{}": {}'.format(name, key, value) + ) elif self.default_file: self.default_config, _ = self.read_config_file(self.default_file) @@ -113,13 +134,15 @@ class Config(UserDict, object): def user_config_file(self): """Get the absolute path to the user config file.""" return os.path.join( - get_user_config_dir(self.app_name, self.app_author), - self.filename) + get_user_config_dir(self.app_name, self.app_author), self.filename + ) def system_config_files(self): """Get a list of absolute paths to the system config files.""" - return [os.path.join(f, self.filename) for f in get_system_config_dirs( - self.app_name, self.app_author)] + return [ + os.path.join(f, self.filename) + for f in get_system_config_dirs(self.app_name, self.app_author) + ] def additional_files(self): """Get a list of absolute paths to the additional config files.""" @@ -127,8 +150,11 @@ class Config(UserDict, object): def all_config_files(self): """Get a list of absolute paths to all the config files.""" - return (self.additional_files() + self.system_config_files() + - [self.user_config_file()]) + return ( + self.additional_files() + + self.system_config_files() + + [self.user_config_file()] + ) def write_default_config(self, overwrite=False): """Write the default config to the user's config file. @@ -139,7 +165,7 @@ class Config(UserDict, object): if not overwrite and os.path.exists(destination): return - with io.open(destination, mode='wb') as f: + with io.open(destination, mode="wb") as f: self.default_config.write(f) def write(self, outfile=None, section=None): @@ -149,7 +175,7 @@ class Config(UserDict, object): :param None/str section: The config section to write, or :data:`None` to write the entire config. """ - with io.open(outfile or self.user_config_file(), 'wb') as f: + with io.open(outfile or self.user_config_file(), "wb") as f: self.data.write(outfile=f, section=section) def read_config_file(self, f): @@ -159,18 +185,21 @@ class Config(UserDict, object): """ configspec = self.default_file if self.validate else None try: - config = ConfigObj(infile=f, configspec=configspec, - interpolation=False, encoding='utf8') + config = ConfigObj( + infile=f, configspec=configspec, interpolation=False, encoding="utf8" + ) + # ConfigObj does not set the encoding on the configspec. + if config.configspec is not None: + config.configspec.encoding = "utf8" except ConfigObjError as e: logger.warning( - 'Unable to parse line {} of config file {}'.format( - e.line_number, f)) + "Unable to parse line {} of config file {}".format(e.line_number, f) + ) config = e.config valid = True if self.validate: - valid = config.validate(Validator(), preserve_errors=True, - copy=True) + valid = config.validate(Validator(), preserve_errors=True, copy=True) if bool(config): self.config_filenames.append(config.filename) @@ -220,15 +249,17 @@ def get_user_config_dir(app_name, app_author, roaming=True, force_xdg=True): """ if WIN: - key = 'APPDATA' if roaming else 'LOCALAPPDATA' - folder = os.path.expanduser(os.environ.get(key, '~')) + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.path.expanduser(os.environ.get(key, "~")) return os.path.join(folder, app_author, app_name) if MAC and not force_xdg: - return os.path.join(os.path.expanduser( - '~/Library/Application Support'), app_name) + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) return os.path.join( - os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')), - _pathify(app_name)) + os.path.expanduser(os.environ.get("XDG_CONFIG_HOME", "~/.config")), + _pathify(app_name), + ) def get_system_config_dirs(app_name, app_author, force_xdg=True): @@ -256,15 +287,15 @@ def get_system_config_dirs(app_name, app_author, force_xdg=True): """ if WIN: - folder = os.environ.get('PROGRAMDATA') + folder = os.environ.get("PROGRAMDATA") return [os.path.join(folder, app_author, app_name)] if MAC and not force_xdg: - return [os.path.join('/Library/Application Support', app_name)] - dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg') + return [os.path.join("/Library/Application Support", app_name)] + dirs = os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg") paths = [os.path.expanduser(x) for x in dirs.split(os.pathsep)] return [os.path.join(d, _pathify(app_name)) for d in paths] def _pathify(s): """Convert spaces to hyphens and lowercase a string.""" - return '-'.join(s.split()).lower() + return "-".join(s.split()).lower() diff --git a/cli_helpers/tabular_output/__init__.py b/cli_helpers/tabular_output/__init__.py index de2f62f..e4247bd 100644 --- a/cli_helpers/tabular_output/__init__.py +++ b/cli_helpers/tabular_output/__init__.py @@ -10,4 +10,4 @@ When formatting data, you'll primarily use the from .output_formatter import format_output, TabularOutputFormatter -__all__ = ['format_output', 'TabularOutputFormatter'] +__all__ = ["format_output", "TabularOutputFormatter"] diff --git a/cli_helpers/tabular_output/delimited_output_adapter.py b/cli_helpers/tabular_output/delimited_output_adapter.py index 098e528..b812456 100644 --- a/cli_helpers/tabular_output/delimited_output_adapter.py +++ b/cli_helpers/tabular_output/delimited_output_adapter.py @@ -8,7 +8,7 @@ from cli_helpers.compat import csv, StringIO from cli_helpers.utils import filter_dict_by_key from .preprocessors import bytes_to_string, override_missing_value -supported_formats = ('csv', 'csv-tab') +supported_formats = ("csv", "csv-tab") preprocessors = (override_missing_value, bytes_to_string) @@ -23,18 +23,26 @@ class linewriter(object): self.line = d -def adapter(data, headers, table_format='csv', **kwargs): +def adapter(data, headers, table_format="csv", **kwargs): """Wrap the formatting inside a function for TabularOutputFormatter.""" - keys = ('dialect', 'delimiter', 'doublequote', 'escapechar', - 'quotechar', 'quoting', 'skipinitialspace', 'strict') - if table_format == 'csv': - delimiter = ',' - elif table_format == 'csv-tab': - delimiter = '\t' + keys = ( + "dialect", + "delimiter", + "doublequote", + "escapechar", + "quotechar", + "quoting", + "skipinitialspace", + "strict", + ) + if table_format == "csv": + delimiter = "," + elif table_format == "csv-tab": + delimiter = "\t" else: - raise ValueError('Invalid table_format specified.') + raise ValueError("Invalid table_format specified.") - ckwargs = {'delimiter': delimiter, 'lineterminator': ''} + ckwargs = {"delimiter": delimiter, "lineterminator": ""} ckwargs.update(filter_dict_by_key(kwargs, keys)) l = linewriter() diff --git a/cli_helpers/tabular_output/output_formatter.py b/cli_helpers/tabular_output/output_formatter.py index fce44b2..6cadf6c 100644 --- a/cli_helpers/tabular_output/output_formatter.py +++ b/cli_helpers/tabular_output/output_formatter.py @@ -4,16 +4,25 @@ 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.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 . import ( + delimited_output_adapter, + vertical_table_adapter, + tabulate_adapter, + tsv_output_adapter, +) from decimal import Decimal import itertools -MISSING_VALUE = '' +MISSING_VALUE = "" MAX_FIELD_WIDTH = 500 TYPES = { @@ -23,12 +32,12 @@ TYPES = { float: 3, Decimal: 3, binary_type: 4, - text_type: 5 + text_type: 5, } OutputFormatHandler = namedtuple( - 'OutputFormatHandler', - 'format_name preprocessors formatter formatter_args') + "OutputFormatHandler", "format_name preprocessors formatter formatter_args" +) class TabularOutputFormatter(object): @@ -96,8 +105,7 @@ class TabularOutputFormatter(object): if format_name in self.supported_formats: self._format_name = format_name else: - raise ValueError('unrecognized format_name "{}"'.format( - format_name)) + raise ValueError('unrecognized format_name "{}"'.format(format_name)) @property def supported_formats(self): @@ -105,8 +113,9 @@ class TabularOutputFormatter(object): return tuple(self._output_formats.keys()) @classmethod - def register_new_formatter(cls, format_name, handler, preprocessors=(), - kwargs=None): + 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. @@ -117,10 +126,18 @@ class TabularOutputFormatter(object): """ 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): + 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 @@ -142,15 +159,13 @@ class TabularOutputFormatter(object): if format_name not in self.supported_formats: raise ValueError('unrecognized format "{}"'.format(format_name)) - (_, _preprocessors, formatter, - fkwargs) = self._output_formats[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) + 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): @@ -197,32 +212,44 @@ def format_output(data, headers, format_name, **kwargs): for vertical_format in vertical_table_adapter.supported_formats: TabularOutputFormatter.register_new_formatter( - vertical_format, vertical_table_adapter.adapter, + vertical_format, + vertical_table_adapter.adapter, vertical_table_adapter.preprocessors, - {'table_format': vertical_format, 'missing_value': MISSING_VALUE, 'max_field_width': None}) + { + "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_format, + delimited_output_adapter.adapter, delimited_output_adapter.preprocessors, - {'table_format': delimited_format, 'missing_value': '', 'max_field_width': None}) + { + "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}) + 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_format, + tsv_output_adapter.adapter, tsv_output_adapter.preprocessors, - {'table_format': tsv_format, 'missing_value': '', 'max_field_width': None}) + {"table_format": tsv_format, "missing_value": "", "max_field_width": None}, + ) diff --git a/cli_helpers/tabular_output/preprocessors.py b/cli_helpers/tabular_output/preprocessors.py index d6d09e0..5d91126 100644 --- a/cli_helpers/tabular_output/preprocessors.py +++ b/cli_helpers/tabular_output/preprocessors.py @@ -7,7 +7,9 @@ from cli_helpers import utils from cli_helpers.compat import text_type, int_types, float_types, HAS_PYGMENTS -def truncate_string(data, headers, max_field_width=None, skip_multiline_string=True, **_): +def truncate_string( + data, headers, max_field_width=None, skip_multiline_string=True, **_ +): """Truncate very long strings. Only needed for tabular representation, because trying to tabulate very long data is problematic in terms of performance, and does not make any @@ -19,8 +21,19 @@ def truncate_string(data, headers, max_field_width=None, skip_multiline_string=T :return: The processed data and headers. :rtype: tuple """ - return (([utils.truncate_string(v, max_field_width, skip_multiline_string) for v in row] for row in data), - [utils.truncate_string(h, max_field_width, skip_multiline_string) for h in headers]) + return ( + ( + [ + utils.truncate_string(v, max_field_width, skip_multiline_string) + for v in row + ] + for row in data + ), + [ + utils.truncate_string(h, max_field_width, skip_multiline_string) + for h in headers + ], + ) def convert_to_string(data, headers, **_): @@ -35,13 +48,20 @@ def convert_to_string(data, headers, **_): :rtype: tuple """ - return (([utils.to_string(v) for v in row] for row in data), - [utils.to_string(h) for h in headers]) + return ( + ([utils.to_string(v) for v in row] for row in data), + [utils.to_string(h) for h in headers], + ) -def override_missing_value(data, headers, style=None, - missing_value_token="Token.Output.Null", - missing_value='', **_): +def override_missing_value( + data, + headers, + style=None, + missing_value_token="Token.Output.Null", + missing_value="", + **_ +): """Override missing values in the *data* with *missing_value*. A missing value is any value that is :data:`None`. @@ -55,12 +75,15 @@ def override_missing_value(data, headers, style=None, :rtype: tuple """ + def fields(): for row in data: processed = [] for field in row: if field is None and style and HAS_PYGMENTS: - styled = utils.style_field(missing_value_token, missing_value, style) + styled = utils.style_field( + missing_value_token, missing_value, style + ) processed.append(styled) elif field is None: processed.append(missing_value) @@ -71,7 +94,7 @@ def override_missing_value(data, headers, style=None, return (fields(), headers) -def override_tab_value(data, headers, new_value=' ', **_): +def override_tab_value(data, headers, new_value=" ", **_): """Override tab values in the *data* with *new_value*. :param iterable data: An :term:`iterable` (e.g. list) of rows. @@ -81,9 +104,13 @@ def override_tab_value(data, headers, new_value=' ', **_): :rtype: tuple """ - return (([v.replace('\t', new_value) if isinstance(v, text_type) else v - for v in row] for row in data), - headers) + return ( + ( + [v.replace("\t", new_value) if isinstance(v, text_type) else v for v in row] + for row in data + ), + headers, + ) def escape_newlines(data, headers, **_): @@ -121,8 +148,10 @@ def bytes_to_string(data, headers, **_): :rtype: tuple """ - return (([utils.bytes_to_string(v) for v in row] for row in data), - [utils.bytes_to_string(h) for h in headers]) + return ( + ([utils.bytes_to_string(v) for v in row] for row in data), + [utils.bytes_to_string(h) for h in headers], + ) def align_decimals(data, headers, column_types=(), **_): @@ -204,17 +233,26 @@ def quote_whitespaces(data, headers, quotestyle="'", **_): for row in data: result = [] for i, v in enumerate(row): - quotation = quotestyle if quote[i] else '' - result.append('{quotestyle}{value}{quotestyle}'.format( - quotestyle=quotation, value=v)) + quotation = quotestyle if quote[i] else "" + result.append( + "{quotestyle}{value}{quotestyle}".format( + quotestyle=quotation, value=v + ) + ) yield result + return results(data), headers -def style_output(data, headers, style=None, - header_token='Token.Output.Header', - odd_row_token='Token.Output.OddRow', - even_row_token='Token.Output.EvenRow', **_): +def style_output( + data, + headers, + style=None, + header_token="Token.Output.Header", + odd_row_token="Token.Output.OddRow", + even_row_token="Token.Output.EvenRow", + **_ +): """Style the *data* and *headers* (e.g. bold, italic, and colors) .. NOTE:: @@ -253,19 +291,32 @@ def style_output(data, headers, style=None, """ from cli_helpers.utils import filter_style_table - relevant_styles = filter_style_table(style, header_token, odd_row_token, even_row_token) + + relevant_styles = filter_style_table( + style, header_token, odd_row_token, even_row_token + ) if style and HAS_PYGMENTS: if relevant_styles.get(header_token): - headers = [utils.style_field(header_token, header, style) for header in headers] + headers = [ + utils.style_field(header_token, header, style) for header in headers + ] if relevant_styles.get(odd_row_token) or relevant_styles.get(even_row_token): - data = ([utils.style_field(odd_row_token if i % 2 else even_row_token, f, style) - for f in r] for i, r in enumerate(data, 1)) + data = ( + [ + utils.style_field( + odd_row_token if i % 2 else even_row_token, f, style + ) + for f in r + ] + for i, r in enumerate(data, 1) + ) return iter(data), headers -def format_numbers(data, headers, column_types=(), integer_format=None, - float_format=None, **_): +def format_numbers( + data, headers, column_types=(), integer_format=None, float_format=None, **_ +): """Format numbers according to a format specification. This uses Python's format specification to format numbers of the following @@ -296,5 +347,7 @@ def format_numbers(data, headers, column_types=(), integer_format=None, return format(field, float_format) return field - data = ([_format_number(v, column_types[i]) for i, v in enumerate(row)] for row in data) + data = ( + [_format_number(v, column_types[i]) for i, v in enumerate(row)] for row in data + ) return data, headers diff --git a/cli_helpers/tabular_output/tabulate_adapter.py b/cli_helpers/tabular_output/tabulate_adapter.py index 8c335b7..92e6f1d 100644 --- a/cli_helpers/tabular_output/tabulate_adapter.py +++ b/cli_helpers/tabular_output/tabulate_adapter.py @@ -4,24 +4,110 @@ from __future__ import unicode_literals from cli_helpers.utils import filter_dict_by_key -from cli_helpers.compat import (Terminal256Formatter, StringIO) -from .preprocessors import (convert_to_string, truncate_string, override_missing_value, - style_output, HAS_PYGMENTS) +from cli_helpers.compat import Terminal256Formatter, StringIO +from .preprocessors import ( + convert_to_string, + truncate_string, + override_missing_value, + style_output, + HAS_PYGMENTS, + escape_newlines, +) import tabulate -supported_markup_formats = ('mediawiki', 'html', 'latex', 'latex_booktabs', - 'textile', 'moinmoin', 'jira') -supported_table_formats = ('plain', 'simple', 'grid', 'fancy_grid', 'pipe', - 'orgtbl', 'psql', 'rst') + +tabulate.MIN_PADDING = 0 + +tabulate._table_formats["psql_unicode"] = tabulate.TableFormat( + lineabove=tabulate.Line("┌", "─", "┬", "┐"), + linebelowheader=tabulate.Line("├", "─", "┼", "┤"), + linebetweenrows=None, + linebelow=tabulate.Line("└", "─", "┴", "┘"), + headerrow=tabulate.DataRow("│", "│", "│"), + datarow=tabulate.DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, +) + +tabulate._table_formats["double"] = tabulate.TableFormat( + lineabove=tabulate.Line("╔", "═", "╦", "╗"), + linebelowheader=tabulate.Line("╠", "═", "╬", "╣"), + linebetweenrows=None, + linebelow=tabulate.Line("╚", "═", "╩", "╝"), + headerrow=tabulate.DataRow("║", "║", "║"), + datarow=tabulate.DataRow("║", "║", "║"), + padding=1, + with_header_hide=None, +) + +tabulate._table_formats["ascii"] = tabulate.TableFormat( + lineabove=tabulate.Line("+", "-", "+", "+"), + linebelowheader=tabulate.Line("+", "-", "+", "+"), + linebetweenrows=None, + linebelow=tabulate.Line("+", "-", "+", "+"), + headerrow=tabulate.DataRow("|", "|", "|"), + datarow=tabulate.DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, +) + +# "minimal" is the same as "plain", but without headers +tabulate._table_formats["minimal"] = tabulate._table_formats["plain"] + +supported_markup_formats = ( + "mediawiki", + "html", + "latex", + "latex_booktabs", + "textile", + "moinmoin", + "jira", +) +supported_table_formats = ( + "ascii", + "plain", + "simple", + "minimal", + "grid", + "fancy_grid", + "pipe", + "orgtbl", + "psql", + "psql_unicode", + "rst", + "github", + "double", +) + supported_formats = supported_markup_formats + supported_table_formats -preprocessors = (override_missing_value, convert_to_string, truncate_string, style_output) +default_kwargs = {"ascii": {"numalign": "left"}} +headless_formats = ("minimal",) + + +def get_preprocessors(format_name): + common_formatters = ( + override_missing_value, + convert_to_string, + truncate_string, + style_output, + ) + + if tabulate.multiline_formats.get(format_name): + return common_formatters + (style_output_table(format_name),) + else: + return common_formatters + (escape_newlines, style_output_table(format_name)) def style_output_table(format_name=""): - def style_output(data, headers, style=None, - table_separator_token='Token.Output.TableSeparator', **_): + def style_output( + data, + headers, + style=None, + table_separator_token="Token.Output.TableSeparator", + **_ + ): """Style the *table* a(e.g. bold, italic, and colors) .. NOTE:: @@ -71,24 +157,28 @@ def style_output_table(format_name=""): if not elt: return elt if elt.__class__ == tabulate.Line: - return tabulate.Line(*(style_field(table_separator_token, val) for val in elt)) + return tabulate.Line( + *(style_field(table_separator_token, val) for val in elt) + ) if elt.__class__ == tabulate.DataRow: - return tabulate.DataRow(*(style_field(table_separator_token, val) for val in elt)) + return tabulate.DataRow( + *(style_field(table_separator_token, val) for val in elt) + ) return elt srcfmt = tabulate._table_formats[format_name] - newfmt = tabulate.TableFormat( - *(addColorInElt(val) for val in srcfmt)) + newfmt = tabulate.TableFormat(*(addColorInElt(val) for val in srcfmt)) tabulate._table_formats[format_name] = newfmt return iter(data), headers + return style_output -def adapter(data, headers, table_format=None, preserve_whitespace=False, - **kwargs): + +def adapter(data, headers, table_format=None, preserve_whitespace=False, **kwargs): """Wrap tabulate inside a function for TabularOutputFormatter.""" - keys = ('floatfmt', 'numalign', 'stralign', 'showindex', 'disable_numparse') - tkwargs = {'tablefmt': table_format} + keys = ("floatfmt", "numalign", "stralign", "showindex", "disable_numparse") + tkwargs = {"tablefmt": table_format} tkwargs.update(filter_dict_by_key(kwargs, keys)) if table_format in supported_markup_formats: @@ -96,4 +186,7 @@ def adapter(data, headers, table_format=None, preserve_whitespace=False, tabulate.PRESERVE_WHITESPACE = preserve_whitespace - return iter(tabulate.tabulate(data, headers, **tkwargs).split('\n')) + tkwargs.update(default_kwargs.get(table_format, {})) + if table_format in headless_formats: + headers = [] + return iter(tabulate.tabulate(data, headers, **tkwargs).split("\n")) diff --git a/cli_helpers/tabular_output/terminaltables_adapter.py b/cli_helpers/tabular_output/terminaltables_adapter.py deleted file mode 100644 index b9c7497..0000000 --- a/cli_helpers/tabular_output/terminaltables_adapter.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -"""Format adapter for the terminaltables module.""" - -from __future__ import unicode_literals - -import terminaltables -import itertools - -from cli_helpers.utils import filter_dict_by_key -from cli_helpers.compat import (Terminal256Formatter, StringIO) -from .preprocessors import (convert_to_string, truncate_string, override_missing_value, - style_output, HAS_PYGMENTS, - override_tab_value, escape_newlines) - -supported_formats = ('ascii', 'double', 'github') -preprocessors = ( - override_missing_value, convert_to_string, override_tab_value, - truncate_string, style_output, escape_newlines -) - -table_format_handler = { - 'ascii': terminaltables.AsciiTable, - 'double': terminaltables.DoubleTable, - 'github': terminaltables.GithubFlavoredMarkdownTable, -} - - -def style_output_table(format_name=""): - def style_output(data, headers, style=None, - table_separator_token='Token.Output.TableSeparator', **_): - """Style the *table* (e.g. bold, italic, and colors) - - .. NOTE:: - This requires the `Pygments `_ library to - be installed. You can install it with CLI Helpers as an extra:: - $ pip install cli_helpers[styles] - - Example usage:: - - from cli_helpers.tabular_output import terminaltables_adapter - from pygments.style import Style - from pygments.token import Token - - class YourStyle(Style): - default_style = "" - styles = { - Token.Output.TableSeparator: '#ansigray' - } - - headers = ('First Name', 'Last Name') - data = [['Fred', 'Roberts'], ['George', 'Smith']] - style_output_table = terminaltables_adapter.style_output_table('psql') - style_output_table(data, headers, style=CliStyle) - - output = terminaltables_adapter.adapter(data, headers, style=YourStyle) - - :param iterable data: An :term:`iterable` (e.g. list) of rows. - :param iterable headers: The column headers. - :param str/pygments.style.Style style: A Pygments style. You can `create - your own styles `_. - :param str table_separator_token: The token type to be used for the table separator. - :return: data and headers. - :rtype: tuple - - """ - if style and HAS_PYGMENTS and format_name in supported_formats: - formatter = Terminal256Formatter(style=style) - - def style_field(token, field): - """Get the styled text for a *field* using *token* type.""" - s = StringIO() - formatter.format(((token, field),), s) - return s.getvalue() - - clss = table_format_handler[format_name] - for char in [char for char in terminaltables.base_table.BaseTable.__dict__ if char.startswith("CHAR_")]: - setattr(clss, char, style_field( - table_separator_token, getattr(clss, char))) - - return iter(data), headers - return style_output - - -def adapter(data, headers, table_format=None, **kwargs): - """Wrap terminaltables inside a function for TabularOutputFormatter.""" - keys = ('title', ) - - table = table_format_handler[table_format] - - t = table([headers] + list(data), **filter_dict_by_key(kwargs, keys)) - - dimensions = terminaltables.width_and_alignment.max_dimensions( - t.table_data, - t.padding_left, - t.padding_right)[:3] - for r in t.gen_table(*dimensions): - yield u''.join(r) diff --git a/cli_helpers/tabular_output/tsv_output_adapter.py b/cli_helpers/tabular_output/tsv_output_adapter.py index 5cdc585..75518b3 100644 --- a/cli_helpers/tabular_output/tsv_output_adapter.py +++ b/cli_helpers/tabular_output/tsv_output_adapter.py @@ -7,10 +7,11 @@ from .preprocessors import bytes_to_string, override_missing_value, convert_to_s from itertools import chain from cli_helpers.utils import replace -supported_formats = ('tsv',) +supported_formats = ("tsv",) preprocessors = (override_missing_value, bytes_to_string, convert_to_string) + def adapter(data, headers, **kwargs): """Wrap the formatting inside a function for TabularOutputFormatter.""" for row in chain((headers,), data): - yield "\t".join((replace(r, (('\n', r'\n'), ('\t', r'\t'))) for r in row)) + yield "\t".join((replace(r, (("\n", r"\n"), ("\t", r"\t"))) for r in row)) diff --git a/cli_helpers/tabular_output/vertical_table_adapter.py b/cli_helpers/tabular_output/vertical_table_adapter.py index a359f7d..0b96cb2 100644 --- a/cli_helpers/tabular_output/vertical_table_adapter.py +++ b/cli_helpers/tabular_output/vertical_table_adapter.py @@ -4,10 +4,9 @@ from __future__ import unicode_literals from cli_helpers.utils import filter_dict_by_key -from .preprocessors import (convert_to_string, override_missing_value, - style_output) +from .preprocessors import convert_to_string, override_missing_value, style_output -supported_formats = ('vertical', ) +supported_formats = ("vertical",) preprocessors = (override_missing_value, convert_to_string, style_output) @@ -21,17 +20,19 @@ def _get_separator(num, sep_title, sep_character, sep_length): title = sep_title.format(n=num + 1) return "{left_divider}[ {title} ]{right_divider}\n".format( - left_divider=left_divider, right_divider=right_divider, title=title) + left_divider=left_divider, right_divider=right_divider, title=title + ) def _format_row(headers, row): """Format a row.""" - formatted_row = [' | '.join(field) for field in zip(headers, row)] - return '\n'.join(formatted_row) + formatted_row = [" | ".join(field) for field in zip(headers, row)] + return "\n".join(formatted_row) -def vertical_table(data, headers, sep_title='{n}. row', sep_character='*', - sep_length=27): +def vertical_table( + data, headers, sep_title="{n}. row", sep_character="*", sep_length=27 +): """Format *data* and *headers* as an vertical table. The values in *data* and *headers* must be strings. @@ -62,5 +63,5 @@ def vertical_table(data, headers, sep_title='{n}. row', sep_character='*', def adapter(data, headers, **kwargs): """Wrap vertical table in a function for TabularOutputFormatter.""" - keys = ('sep_title', 'sep_character', 'sep_length') + keys = ("sep_title", "sep_character", "sep_length") return vertical_table(data, headers, **filter_dict_by_key(kwargs, keys)) diff --git a/cli_helpers/utils.py b/cli_helpers/utils.py index f11fa40..3f09cb5 100644 --- a/cli_helpers/utils.py +++ b/cli_helpers/utils.py @@ -7,6 +7,7 @@ from functools import lru_cache from typing import Dict from typing import TYPE_CHECKING + if TYPE_CHECKING: from pygments.style import StyleMeta @@ -20,10 +21,16 @@ def bytes_to_string(b): """ if isinstance(b, binary_type): + needs_hex = False try: - return b.decode('utf8') + result = b.decode("utf8") + needs_hex = not result.isprintable() except UnicodeDecodeError: - return '0x' + binascii.hexlify(b).decode('ascii') + needs_hex = True + if needs_hex: + return "0x" + binascii.hexlify(b).decode("ascii") + else: + return result return b @@ -37,16 +44,20 @@ def to_string(value): def truncate_string(value, max_width=None, skip_multiline_string=True): """Truncate string values.""" - if skip_multiline_string and isinstance(value, text_type) and '\n' in value: + if skip_multiline_string and isinstance(value, text_type) and "\n" in value: return value - elif isinstance(value, text_type) and max_width is not None and len(value) > max_width: - return value[:max_width-3] + "..." + elif ( + isinstance(value, text_type) + and max_width is not None + and len(value) > max_width + ): + return value[: max_width - 3] + "..." return value def intlen(n): """Find the length of the integer part of a number *n*.""" - pos = n.find('.') + pos = n.find(".") return len(n) if pos < 0 else pos @@ -61,12 +72,12 @@ def unique_items(seq): return [x for x in seq if not (x in seen or seen.add(x))] -_ansi_re = re.compile('\033\\[((?:\\d|;)*)([a-zA-Z])') +_ansi_re = re.compile("\033\\[((?:\\d|;)*)([a-zA-Z])") def strip_ansi(value): """Strip the ANSI escape sequences from a string.""" - return _ansi_re.sub('', value) + return _ansi_re.sub("", value) def replace(s, replace): @@ -98,9 +109,8 @@ def filter_style_table(style: "StyleMeta", *relevant_styles: str) -> Dict: 'Token.Output.OddRow': "", } """ - _styles_iter = ((str(key), val) for key, val in getattr(style, 'styles', {}).items()) - _relevant_styles_iter = filter( - lambda tpl: tpl[0] in relevant_styles, - _styles_iter + _styles_iter = ( + (str(key), val) for key, val in getattr(style, "styles", {}).items() ) + _relevant_styles_iter = filter(lambda tpl: tpl[0] in relevant_styles, _styles_iter) return {key: val for key, val in _relevant_styles_iter} -- cgit v1.2.3