214 lines
7.4 KiB
Python
214 lines
7.4 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.
|
|
|
|
"""Output formatters using prettytable."""
|
|
|
|
import os
|
|
|
|
import prettytable
|
|
|
|
from cliff import utils
|
|
from . import base
|
|
from cliff import columns
|
|
|
|
|
|
def _format_row(row):
|
|
new_row = []
|
|
for r in row:
|
|
if isinstance(r, columns.FormattableColumn):
|
|
r = r.human_readable()
|
|
if isinstance(r, str):
|
|
r = r.replace('\r\n', '\n').replace('\r', ' ')
|
|
new_row.append(r)
|
|
return new_row
|
|
|
|
|
|
class TableFormatter(base.ListFormatter, base.SingleFormatter):
|
|
|
|
ALIGNMENTS = {
|
|
int: 'r',
|
|
str: 'l',
|
|
float: 'r',
|
|
}
|
|
|
|
def add_argument_group(self, parser):
|
|
group = parser.add_argument_group('table formatter')
|
|
group.add_argument(
|
|
'--max-width',
|
|
metavar='<integer>',
|
|
default=int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0)),
|
|
type=int,
|
|
help=('Maximum display width, <1 to disable. You can also '
|
|
'use the CLIFF_MAX_TERM_WIDTH environment variable, '
|
|
'but the parameter takes precedence.'),
|
|
)
|
|
group.add_argument(
|
|
'--fit-width',
|
|
action='store_true',
|
|
default=bool(int(os.environ.get('CLIFF_FIT_WIDTH', 0))),
|
|
help=('Fit the table to the display width. '
|
|
'Implied if --max-width greater than 0. '
|
|
'Set the environment variable CLIFF_FIT_WIDTH=1 '
|
|
'to always enable'),
|
|
)
|
|
group.add_argument(
|
|
'--print-empty',
|
|
action='store_true',
|
|
help='Print empty table if there is no data to show.',
|
|
)
|
|
|
|
def add_rows(self, table, column_names, data):
|
|
# Figure out the types of the columns in the
|
|
# first row and set the alignment of the
|
|
# output accordingly.
|
|
data_iter = iter(data)
|
|
try:
|
|
first_row = next(data_iter)
|
|
except StopIteration:
|
|
pass
|
|
else:
|
|
for value, name in zip(first_row, column_names):
|
|
alignment = self.ALIGNMENTS.get(type(value), 'l')
|
|
table.align[name] = alignment
|
|
# Now iterate over the data and add the rows.
|
|
table.add_row(_format_row(first_row))
|
|
for row in data_iter:
|
|
table.add_row(_format_row(row))
|
|
|
|
def emit_list(self, column_names, data, stdout, parsed_args):
|
|
x = prettytable.PrettyTable(
|
|
column_names,
|
|
print_empty=parsed_args.print_empty,
|
|
)
|
|
x.padding_width = 1
|
|
|
|
# Add rows if data is provided
|
|
if data:
|
|
self.add_rows(x, column_names, data)
|
|
|
|
# Choose a reasonable min_width to better handle many columns on a
|
|
# narrow console. The table will overflow the console width in
|
|
# preference to wrapping columns smaller than 8 characters.
|
|
min_width = 8
|
|
self._assign_max_widths(
|
|
x, int(parsed_args.max_width), min_width,
|
|
parsed_args.fit_width)
|
|
|
|
formatted = x.get_string()
|
|
stdout.write(formatted)
|
|
stdout.write('\n')
|
|
return
|
|
|
|
def emit_one(self, column_names, data, stdout, parsed_args):
|
|
x = prettytable.PrettyTable(field_names=('Field', 'Value'),
|
|
print_empty=False)
|
|
x.padding_width = 1
|
|
# Align all columns left because the values are
|
|
# not all the same type.
|
|
x.align['Field'] = 'l'
|
|
x.align['Value'] = 'l'
|
|
for name, value in zip(column_names, data):
|
|
x.add_row(_format_row((name, value)))
|
|
|
|
# Choose a reasonable min_width to better handle a narrow
|
|
# console. The table will overflow the console width in preference
|
|
# to wrapping columns smaller than 16 characters in an attempt to keep
|
|
# the Field column readable.
|
|
min_width = 16
|
|
self._assign_max_widths(
|
|
x, int(parsed_args.max_width), min_width,
|
|
parsed_args.fit_width)
|
|
|
|
formatted = x.get_string()
|
|
stdout.write(formatted)
|
|
stdout.write('\n')
|
|
return
|
|
|
|
@staticmethod
|
|
def _field_widths(field_names, first_line):
|
|
|
|
# use the first line +----+-------+ to infer column widths
|
|
# accounting for padding and dividers
|
|
widths = [max(0, len(i) - 2) for i in first_line.split('+')[1:-1]]
|
|
return dict(zip(field_names, widths))
|
|
|
|
@staticmethod
|
|
def _width_info(term_width, field_count):
|
|
# remove padding and dividers for width available to actual content
|
|
usable_total_width = max(0, term_width - 1 - 3 * field_count)
|
|
|
|
# calculate width per column if all columns were equal
|
|
if field_count == 0:
|
|
optimal_width = 0
|
|
else:
|
|
optimal_width = max(0, usable_total_width // field_count)
|
|
|
|
return usable_total_width, optimal_width
|
|
|
|
@staticmethod
|
|
def _build_shrink_fields(usable_total_width, optimal_width,
|
|
field_widths, field_names):
|
|
shrink_fields = []
|
|
shrink_remaining = usable_total_width
|
|
for field in field_names:
|
|
w = field_widths[field]
|
|
if w <= optimal_width:
|
|
# leave alone columns which are smaller than the optimal width
|
|
shrink_remaining -= w
|
|
else:
|
|
shrink_fields.append(field)
|
|
|
|
return shrink_fields, shrink_remaining
|
|
|
|
@staticmethod
|
|
def _assign_max_widths(x, max_width, min_width=0, fit_width=False):
|
|
"""Set maximum widths for columns of table `x`,
|
|
with the last column recieving either leftover columns
|
|
or `min_width`, depending on what offers more space.
|
|
"""
|
|
if max_width > 0:
|
|
term_width = max_width
|
|
elif not fit_width:
|
|
# Fitting is disabled
|
|
return
|
|
else:
|
|
term_width = utils.terminal_width()
|
|
if not term_width:
|
|
# not a tty, so do not set any max widths
|
|
return
|
|
field_count = len(x.field_names)
|
|
|
|
try:
|
|
first_line = x.get_string().splitlines()[0]
|
|
if len(first_line) <= term_width:
|
|
return
|
|
except IndexError:
|
|
return
|
|
|
|
usable_total_width, optimal_width = TableFormatter._width_info(
|
|
term_width, field_count)
|
|
|
|
field_widths = TableFormatter._field_widths(x.field_names, first_line)
|
|
|
|
shrink_fields, shrink_remaining = TableFormatter._build_shrink_fields(
|
|
usable_total_width, optimal_width, field_widths, x.field_names)
|
|
|
|
shrink_to = shrink_remaining // len(shrink_fields)
|
|
# make all shrinkable fields size shrink_to apart from the last one
|
|
for field in shrink_fields[:-1]:
|
|
x.max_width[field] = max(min_width, shrink_to)
|
|
shrink_remaining -= shrink_to
|
|
|
|
# give the last shrinkable column any remaining shrink or min_width
|
|
field = shrink_fields[-1]
|
|
x.max_width[field] = max(min_width, shrink_remaining)
|