df4616b31e75c14ae8cc08f19de55873460cb694
[sdc/sdc-distribution-client.git] /
1 #  Licensed under the Apache License, Version 2.0 (the "License"); you may
2 #  not use this file except in compliance with the License. You may obtain
3 #  a copy of the License at
4 #
5 #       http://www.apache.org/licenses/LICENSE-2.0
6 #
7 #  Unless required by applicable law or agreed to in writing, software
8 #  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 #  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 #  License for the specific language governing permissions and limitations
11 #  under the License.
12
13 """Output formatters using prettytable.
14 """
15
16 import prettytable
17 import six
18 import os
19
20 from cliff import utils
21 from . import base
22 from cliff import columns
23
24
25 def _format_row(row):
26     new_row = []
27     for r in row:
28         if isinstance(r, columns.FormattableColumn):
29             r = r.human_readable()
30         if isinstance(r, six.string_types):
31             r = r.replace('\r\n', '\n').replace('\r', ' ')
32         new_row.append(r)
33     return new_row
34
35
36 class TableFormatter(base.ListFormatter, base.SingleFormatter):
37
38     ALIGNMENTS = {
39         int: 'r',
40         str: 'l',
41         float: 'r',
42     }
43     try:
44         ALIGNMENTS[unicode] = 'l'
45     except NameError:
46         pass
47
48     def add_argument_group(self, parser):
49         group = parser.add_argument_group('table formatter')
50         group.add_argument(
51             '--max-width',
52             metavar='<integer>',
53             default=int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0)),
54             type=int,
55             help=('Maximum display width, <1 to disable. You can also '
56                   'use the CLIFF_MAX_TERM_WIDTH environment variable, '
57                   'but the parameter takes precedence.'),
58         )
59         group.add_argument(
60             '--print-empty',
61             action='store_true',
62             help='Print empty table if there is no data to show.',
63         )
64
65     def add_rows(self, table, column_names, data):
66         # Figure out the types of the columns in the
67         # first row and set the alignment of the
68         # output accordingly.
69         data_iter = iter(data)
70         try:
71             first_row = next(data_iter)
72         except StopIteration:
73             pass
74         else:
75             for value, name in zip(first_row, column_names):
76                 alignment = self.ALIGNMENTS.get(type(value), 'l')
77                 table.align[name] = alignment
78             # Now iterate over the data and add the rows.
79             table.add_row(_format_row(first_row))
80             for row in data_iter:
81                 table.add_row(_format_row(row))
82
83     def emit_list(self, column_names, data, stdout, parsed_args):
84         x = prettytable.PrettyTable(
85             column_names,
86             print_empty=parsed_args.print_empty,
87         )
88         x.padding_width = 1
89
90         # Add rows if data is provided
91         if data:
92             self.add_rows(x, column_names, data)
93
94         # Choose a reasonable min_width to better handle many columns on a
95         # narrow console. The table will overflow the console width in
96         # preference to wrapping columns smaller than 8 characters.
97         min_width = 8
98         self._assign_max_widths(
99             stdout, x, int(parsed_args.max_width), min_width)
100
101         formatted = x.get_string()
102         stdout.write(formatted)
103         stdout.write('\n')
104         return
105
106     def emit_one(self, column_names, data, stdout, parsed_args):
107         x = prettytable.PrettyTable(field_names=('Field', 'Value'),
108                                     print_empty=False)
109         x.padding_width = 1
110         # Align all columns left because the values are
111         # not all the same type.
112         x.align['Field'] = 'l'
113         x.align['Value'] = 'l'
114         for name, value in zip(column_names, data):
115             x.add_row(_format_row((name, value)))
116
117         # Choose a reasonable min_width to better handle a narrow
118         # console. The table will overflow the console width in preference
119         # to wrapping columns smaller than 16 characters in an attempt to keep
120         # the Field column readable.
121         min_width = 16
122         self._assign_max_widths(
123             stdout, x, int(parsed_args.max_width), min_width)
124
125         formatted = x.get_string()
126         stdout.write(formatted)
127         stdout.write('\n')
128         return
129
130     @staticmethod
131     def _field_widths(field_names, first_line):
132
133         # use the first line +----+-------+ to infer column widths
134         # accounting for padding and dividers
135         widths = [max(0, len(i) - 2) for i in first_line.split('+')[1:-1]]
136         return dict(zip(field_names, widths))
137
138     @staticmethod
139     def _width_info(term_width, field_count):
140         # remove padding and dividers for width available to actual content
141         usable_total_width = max(0, term_width - 1 - 3 * field_count)
142
143         # calculate width per column if all columns were equal
144         if field_count == 0:
145             optimal_width = 0
146         else:
147             optimal_width = max(0, usable_total_width // field_count)
148
149         return usable_total_width, optimal_width
150
151     @staticmethod
152     def _build_shrink_fields(usable_total_width, optimal_width,
153                              field_widths, field_names):
154         shrink_fields = []
155         shrink_remaining = usable_total_width
156         for field in field_names:
157             w = field_widths[field]
158             if w <= optimal_width:
159                 # leave alone columns which are smaller than the optimal width
160                 shrink_remaining -= w
161             else:
162                 shrink_fields.append(field)
163
164         return shrink_fields, shrink_remaining
165
166     @staticmethod
167     def _assign_max_widths(stdout, x, max_width, min_width=0):
168         if min_width:
169             x.min_width = min_width
170
171         if max_width > 0:
172             term_width = max_width
173         else:
174             term_width = utils.terminal_width(stdout)
175             if not term_width:
176                 # not a tty, so do not set any max widths
177                 return
178         field_count = len(x.field_names)
179
180         try:
181             first_line = x.get_string().splitlines()[0]
182             if len(first_line) <= term_width:
183                 return
184         except IndexError:
185             return
186
187         usable_total_width, optimal_width = TableFormatter._width_info(
188             term_width, field_count)
189
190         field_widths = TableFormatter._field_widths(x.field_names, first_line)
191
192         shrink_fields, shrink_remaining = TableFormatter._build_shrink_fields(
193             usable_total_width, optimal_width, field_widths, x.field_names)
194
195         shrink_to = shrink_remaining // len(shrink_fields)
196         # make all shrinkable fields size shrink_to apart from the last one
197         for field in shrink_fields[:-1]:
198             x.max_width[field] = max(min_width, shrink_to)
199             shrink_remaining -= shrink_to
200
201         # give the last shrinkable column shrink_to plus any remaining
202         field = shrink_fields[-1]
203         x.max_width[field] = max(min_width, shrink_remaining)