Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/table.py
1566 views
1
# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
14
import struct
15
import sys
16
import unicodedata
17
18
import colorama
19
20
from awscli.utils import is_a_tty
21
22
# `autoreset` allows us to not have to sent reset sequences for every
23
# string. `strip` lets us preserve color when redirecting.
24
COLORAMA_KWARGS = {
25
'autoreset': True,
26
'strip': False,
27
}
28
29
30
def get_text_length(text):
31
# `len(unichar)` measures the number of characters, so we use
32
# `unicodedata.east_asian_width` to measure the length of characters.
33
# Following responses are considered to be full-width length.
34
# * A(Ambiguous)
35
# * F(Fullwidth)
36
# * W(Wide)
37
text = str(text)
38
return sum(
39
2 if unicodedata.east_asian_width(char) in 'WFA' else 1
40
for char in text
41
)
42
43
44
def determine_terminal_width(default_width=80):
45
# If we can't detect the terminal width, the default_width is returned.
46
try:
47
from fcntl import ioctl
48
from termios import TIOCGWINSZ
49
except ImportError:
50
return default_width
51
try:
52
height, width = struct.unpack(
53
'hhhh', ioctl(sys.stdout, TIOCGWINSZ, '\000' * 8)
54
)[0:2]
55
except Exception:
56
return default_width
57
else:
58
return width
59
60
61
def center_text(
62
text, length=80, left_edge='|', right_edge='|', text_length=None
63
):
64
"""Center text with specified edge chars.
65
66
You can pass in the length of the text as an arg, otherwise it is computed
67
automatically for you. This can allow you to center a string not based
68
on it's literal length (useful if you're using ANSI codes).
69
"""
70
# postcondition: get_text_length(returned_text) == length
71
if text_length is None:
72
text_length = get_text_length(text)
73
output = []
74
char_start = (length // 2) - (text_length // 2) - 1
75
output.append(left_edge + ' ' * char_start + text)
76
length_so_far = get_text_length(left_edge) + char_start + text_length
77
right_side_spaces = length - get_text_length(right_edge) - length_so_far
78
output.append(' ' * right_side_spaces)
79
output.append(right_edge)
80
final = ''.join(output)
81
return final
82
83
84
def align_left(
85
text,
86
length,
87
left_edge='|',
88
right_edge='|',
89
text_length=None,
90
left_padding=2,
91
):
92
"""Left align text."""
93
# postcondition: get_text_length(returned_text) == length
94
if text_length is None:
95
text_length = get_text_length(text)
96
computed_length = (
97
text_length
98
+ left_padding
99
+ get_text_length(left_edge)
100
+ get_text_length(right_edge)
101
)
102
if length - computed_length >= 0:
103
padding = left_padding
104
else:
105
padding = 0
106
output = []
107
length_so_far = 0
108
output.append(left_edge)
109
length_so_far += len(left_edge)
110
output.append(' ' * padding)
111
length_so_far += padding
112
output.append(text)
113
length_so_far += text_length
114
output.append(' ' * (length - length_so_far - len(right_edge)))
115
output.append(right_edge)
116
return ''.join(output)
117
118
119
def convert_to_vertical_table(sections):
120
# Any section that only has a single row is
121
# inverted, so:
122
# header1 | header2 | header3
123
# val1 | val2 | val2
124
#
125
# becomes:
126
#
127
# header1 | val1
128
# header2 | val2
129
# header3 | val3
130
for i, section in enumerate(sections):
131
if len(section.rows) == 1 and section.headers:
132
headers = section.headers
133
new_section = Section()
134
new_section.title = section.title
135
new_section.indent_level = section.indent_level
136
for header, element in zip(headers, section.rows[0]):
137
new_section.add_row([header, element])
138
sections[i] = new_section
139
140
141
class IndentedStream:
142
def __init__(
143
self, stream, indent_level, left_indent_char='|', right_indent_char='|'
144
):
145
self._stream = stream
146
self._indent_level = indent_level
147
self._left_indent_char = left_indent_char
148
self._right_indent_char = right_indent_char
149
150
def write(self, text):
151
self._stream.write(self._left_indent_char * self._indent_level)
152
if text.endswith('\n'):
153
self._stream.write(text[:-1])
154
self._stream.write(self._right_indent_char * self._indent_level)
155
self._stream.write('\n')
156
else:
157
self._stream.write(text)
158
159
def __getattr__(self, attr):
160
return getattr(self._stream, attr)
161
162
163
class Styler:
164
def style_title(self, text):
165
return text
166
167
def style_header_column(self, text):
168
return text
169
170
def style_row_element(self, text):
171
return text
172
173
def style_indentation_char(self, text):
174
return text
175
176
177
class ColorizedStyler(Styler):
178
def __init__(self):
179
colorama.init(**COLORAMA_KWARGS)
180
181
def style_title(self, text):
182
# Originally bold + underline
183
return text
184
185
def style_header_column(self, text):
186
# Originally underline
187
return text
188
189
def style_row_element(self, text):
190
return (
191
colorama.Style.BRIGHT
192
+ colorama.Fore.BLUE
193
+ text
194
+ colorama.Style.RESET_ALL
195
)
196
197
def style_indentation_char(self, text):
198
return (
199
colorama.Style.DIM
200
+ colorama.Fore.YELLOW
201
+ text
202
+ colorama.Style.RESET_ALL
203
)
204
205
206
class MultiTable:
207
def __init__(
208
self,
209
terminal_width=None,
210
initial_section=True,
211
column_separator='|',
212
terminal=None,
213
styler=None,
214
auto_reformat=True,
215
):
216
self._auto_reformat = auto_reformat
217
if initial_section:
218
self._current_section = Section()
219
self._sections = [self._current_section]
220
else:
221
self._current_section = None
222
self._sections = []
223
if styler is None:
224
# Move out to factory.
225
if is_a_tty():
226
self._styler = ColorizedStyler()
227
else:
228
self._styler = Styler()
229
else:
230
self._styler = styler
231
self._rendering_index = 0
232
self._column_separator = column_separator
233
if terminal_width is None:
234
self._terminal_width = determine_terminal_width()
235
236
def add_title(self, title):
237
self._current_section.add_title(title)
238
239
def add_row_header(self, headers):
240
self._current_section.add_header(headers)
241
242
def add_row(self, row_elements):
243
self._current_section.add_row(row_elements)
244
245
def new_section(self, title, indent_level=0):
246
self._current_section = Section()
247
self._sections.append(self._current_section)
248
self._current_section.add_title(title)
249
self._current_section.indent_level = indent_level
250
251
def render(self, stream):
252
max_width = self._calculate_max_width()
253
should_convert_table = self._determine_conversion_needed(max_width)
254
if should_convert_table:
255
convert_to_vertical_table(self._sections)
256
max_width = self._calculate_max_width()
257
stream.write('-' * max_width + '\n')
258
for section in self._sections:
259
self._render_section(section, max_width, stream)
260
261
def _determine_conversion_needed(self, max_width):
262
# If we don't know the width of the controlling terminal,
263
# then we don't try to resize the table.
264
if max_width > self._terminal_width:
265
return self._auto_reformat
266
267
def _calculate_max_width(self):
268
max_width = max(
269
s.total_width(
270
padding=4, with_border=True, outer_padding=s.indent_level
271
)
272
for s in self._sections
273
)
274
return max_width
275
276
def _render_section(self, section, max_width, stream):
277
stream = IndentedStream(
278
stream,
279
section.indent_level,
280
self._styler.style_indentation_char('|'),
281
self._styler.style_indentation_char('|'),
282
)
283
max_width -= section.indent_level * 2
284
self._render_title(section, max_width, stream)
285
self._render_column_titles(section, max_width, stream)
286
self._render_rows(section, max_width, stream)
287
288
def _render_title(self, section, max_width, stream):
289
# The title consists of:
290
# title : | This is the title |
291
# bottom_border: ----------------------------
292
if section.title:
293
title = self._styler.style_title(section.title)
294
stream.write(
295
center_text(
296
title, max_width, '|', '|', get_text_length(section.title)
297
)
298
+ '\n'
299
)
300
if not section.headers and not section.rows:
301
stream.write('+%s+' % ('-' * (max_width - 2)) + '\n')
302
303
def _render_column_titles(self, section, max_width, stream):
304
if not section.headers:
305
return
306
# In order to render the column titles we need to know
307
# the width of each of the columns.
308
widths = section.calculate_column_widths(
309
padding=4, max_width=max_width
310
)
311
# TODO: Built a list instead of +=, it's more efficient.
312
current = ''
313
length_so_far = 0
314
# The first cell needs both left and right edges '| foo |'
315
# while subsequent cells only need right edges ' foo |'.
316
first = True
317
for width, header in zip(widths, section.headers):
318
stylized_header = self._styler.style_header_column(header)
319
if first:
320
left_edge = '|'
321
first = False
322
else:
323
left_edge = ''
324
current += center_text(
325
text=stylized_header,
326
length=width,
327
left_edge=left_edge,
328
right_edge='|',
329
text_length=get_text_length(header),
330
)
331
length_so_far += width
332
self._write_line_break(stream, widths)
333
stream.write(current + '\n')
334
335
def _write_line_break(self, stream, widths):
336
# Write out something like:
337
# +-------+---------+---------+
338
parts = []
339
first = True
340
for width in widths:
341
if first:
342
parts.append('+%s+' % ('-' * (width - 2)))
343
first = False
344
else:
345
parts.append('%s+' % ('-' * (width - 1)))
346
parts.append('\n')
347
stream.write(''.join(parts))
348
349
def _render_rows(self, section, max_width, stream):
350
if not section.rows:
351
return
352
widths = section.calculate_column_widths(
353
padding=4, max_width=max_width
354
)
355
if not widths:
356
return
357
self._write_line_break(stream, widths)
358
for row in section.rows:
359
# TODO: Built the string in a list then join instead of using +=,
360
# it's more efficient.
361
current = ''
362
length_so_far = 0
363
first = True
364
for width, element in zip(widths, row):
365
if first:
366
left_edge = '|'
367
first = False
368
else:
369
left_edge = ''
370
stylized = self._styler.style_row_element(element)
371
current += align_left(
372
text=stylized,
373
length=width,
374
left_edge=left_edge,
375
right_edge=self._column_separator,
376
text_length=get_text_length(element),
377
)
378
length_so_far += width
379
stream.write(current + '\n')
380
self._write_line_break(stream, widths)
381
382
383
class Section:
384
def __init__(self):
385
self.title = ''
386
self.headers = []
387
self.rows = []
388
self.indent_level = 0
389
self._num_cols = None
390
self._max_widths = []
391
392
def __repr__(self):
393
return (
394
f"Section(title={self.title}, headers={self.headers}, "
395
f"indent_level={self.indent_level}, num_rows={len(self.rows)})"
396
)
397
398
def calculate_column_widths(self, padding=0, max_width=None):
399
# postcondition: sum(widths) == max_width
400
unscaled_widths = [w + padding for w in self._max_widths]
401
if max_width is None:
402
return unscaled_widths
403
if not unscaled_widths:
404
return unscaled_widths
405
else:
406
# Compute scale factor for max_width.
407
scale_factor = max_width / float(sum(unscaled_widths))
408
scaled = [int(round(scale_factor * w)) for w in unscaled_widths]
409
# Once we've scaled the columns, we may be slightly over/under
410
# the amount we need so we have to adjust the columns.
411
off_by = sum(scaled) - max_width
412
while off_by != 0:
413
iter_order = range(len(scaled))
414
if off_by < 0:
415
iter_order = reversed(iter_order)
416
for i in iter_order:
417
if off_by > 0:
418
scaled[i] -= 1
419
off_by -= 1
420
else:
421
scaled[i] += 1
422
off_by += 1
423
if off_by == 0:
424
break
425
return scaled
426
427
def total_width(self, padding=0, with_border=False, outer_padding=0):
428
total = 0
429
# One char on each side == 2 chars total to the width.
430
border_padding = 2
431
for w in self.calculate_column_widths():
432
total += w + padding
433
if with_border:
434
total += border_padding
435
total += outer_padding + outer_padding
436
return max(
437
get_text_length(self.title)
438
+ border_padding
439
+ outer_padding
440
+ outer_padding,
441
total,
442
)
443
444
def add_title(self, title):
445
self.title = title
446
447
def add_header(self, headers):
448
self._update_max_widths(headers)
449
if self._num_cols is None:
450
self._num_cols = len(headers)
451
self.headers = self._format_headers(headers)
452
453
def _format_headers(self, headers):
454
return headers
455
456
def add_row(self, row):
457
if self._num_cols is None:
458
self._num_cols = len(row)
459
if len(row) != self._num_cols:
460
raise ValueError(
461
f"Row should have {self._num_cols} elements, instead "
462
f"it has {len(row)}"
463
)
464
row = self._format_row(row)
465
self.rows.append(row)
466
self._update_max_widths(row)
467
468
def _format_row(self, row):
469
return [str(r) for r in row]
470
471
def _update_max_widths(self, row):
472
if not self._max_widths:
473
self._max_widths = [get_text_length(el) for el in row]
474
else:
475
for i, el in enumerate(row):
476
self._max_widths[i] = max(
477
get_text_length(el), self._max_widths[i]
478
)
479
480