Ian Cordasco 683f342506 Make column ordering consistent and predictable
This changes how we sort columns in listings and how we display the
column headings. The default columns are now stored as lists so that
they are the same from run to run. This simplifies some of the logic in
the shell modules as well. Instead of keeping static mappings of
attributes to column headings, we now use some simple python logic, to
title case the columns.

This commit covers:

- cell-* commands
- cloud-* commands
- host-* commands
- project-* commands
- region-* commands

Finally, we noticed that the cloud-list and region-list commands were
behaving differently from the rest of the -list commands. This
unifies the interface to add the --detail flag.

Closes-bug: #1659103
Closes-bug: #1659427
Closes-bug: #1668221
Change-Id: If5906780e501c7b9ba93ecf54a7bcf6db5ddfa1c
2017-03-03 10:25:53 -06:00

182 lines
6.5 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Pretty-table formatter implementation for the craton CLI."""
from __future__ import print_function
import textwrap
from oslo_utils import encodeutils
import prettytable
import six
from cratonclient.common import cliutils
from cratonclient.formatters import base
class Formatter(base.Formatter):
"""Implementation of the default table-style formatter."""
def after_init(self):
"""Set-up after initialization."""
self.fields = []
self.formatters = {}
self.sortby_index = None
self.mixed_case_fields = set([])
self.field_labels = []
self.dict_property = "Property"
self.wrap = 0
self.dict_value = "Value"
def configure(self, fields=None, formatters=None, sortby_index=False,
mixed_case_fields=None, field_labels=None,
dict_property=None, dict_value=None, wrap=None):
"""Configure some of the settings used to print the tables.
Parameters that configure list presentation:
:param list fields:
List of field names as strings.
:param dict formatters:
Mapping of field names to formatter functions that accept the
resource.
:param int sortby_index:
The index of the field name in :param:`fields` to sort the table
rows by. If ``None``, PrettyTable will not sort the items at all.
:param list mixed_case_fields:
List of field names also in :param:`fields` that are mixed case
and need preprocessing prior to retrieving the attribute.
:param list field_labels:
List of field labels that need to match :param:`fields`.
Parameters that configure the plain resource representation:
:param str dict_property:
The name of the first column.
:param str dict_value:
The name of the second column.
:param int wrap:
Length at which to wrap the second column.
All of these may be specified, but will be ignored based on how the
formatter is executed.
"""
if fields is not None:
self.fields = fields
if field_labels is None:
self.field_labels = cliutils.field_labels_from(self.fields)
elif len(field_labels) != len(self.fields):
raise ValueError(
"Field labels list %(labels)s has different number "
"of elements than fields list %(fields)s" %
{'labels': field_labels, 'fields': fields}
)
else:
self.field_labels = field_labels
if formatters is not None:
self.formatters = formatters
if sortby_index is not False:
try:
sortby_index = int(sortby_index)
except TypeError:
if sortby_index is not None:
raise ValueError(
'sortby_index must be None or an integer'
)
except ValueError:
raise
else:
if self.field_labels and (
sortby_index < 0 or
sortby_index > len(self.field_labels)):
raise ValueError(
'sortby_index must be a non-negative number less '
'than {}'.format(len(self.field_labels))
)
self.sortby_index = sortby_index
if mixed_case_fields is not None:
self.mixed_case_fields = set(mixed_case_fields)
if dict_property is not None:
self.dict_property = dict_property
if dict_value is not None:
self.dict_value = dict_value
if wrap is not None:
self.wrap = wrap
return self
def sortby_kwargs(self):
"""Generate the sortby keyword argument for PrettyTable."""
if self.sortby_index is None:
return {}
return {'sortby': self.field_labels[self.sortby_index]}
def build_table(self, field_labels, alignment='l'):
"""Create a PrettyTable instance based off of the labels."""
table = prettytable.PrettyTable(field_labels)
table.align = alignment
return table
def handle_generator(self, generator):
"""Handle a generator of resources."""
sortby_kwargs = self.sortby_kwargs()
table = self.build_table(self.field_labels)
for resource in generator:
row = []
for field in self.fields:
formatter = self.formatters.get(field)
if formatter is not None:
data = formatter(resource)
else:
if field in self.mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
data = getattr(resource, field_name, '')
row.append(data)
table.add_row(row)
output = encodeutils.safe_encode(table.get_string(**sortby_kwargs))
if six.PY3:
output = output.decode()
print(output)
def handle_instance(self, instance):
"""Handle a single resource."""
table = self.build_table([self.dict_property, self.dict_value])
for key, value in sorted(instance.to_dict().items()):
if isinstance(value, dict):
value = six.text_type(value)
if self.wrap > 0:
value = textwrap.fill(six.text_type(value), self.wrap)
if value and isinstance(value, six.string_types) and '\n' in value:
lines = value.strip().split('\n')
column1 = key
for line in lines:
table.add_row([column1, line])
column1 = ''
else:
table.add_row([key, value])
output = encodeutils.safe_encode(table.get_string())
if six.PY3:
output = output.decode()
print(output)