Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/humanfriendly/tables.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author | shellac |
---|---|
date | Sat, 02 May 2020 07:14:21 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:26e78fe6e8c4 |
---|---|
1 # Human friendly input/output in Python. | |
2 # | |
3 # Author: Peter Odding <peter@peterodding.com> | |
4 # Last Change: February 16, 2020 | |
5 # URL: https://humanfriendly.readthedocs.io | |
6 | |
7 """ | |
8 Functions that render ASCII tables. | |
9 | |
10 Some generic notes about the table formatting functions in this module: | |
11 | |
12 - These functions were not written with performance in mind (*at all*) because | |
13 they're intended to format tabular data to be presented on a terminal. If | |
14 someone were to run into a performance problem using these functions, they'd | |
15 be printing so much tabular data to the terminal that a human wouldn't be | |
16 able to digest the tabular data anyway, so the point is moot :-). | |
17 | |
18 - These functions ignore ANSI escape sequences (at least the ones generated by | |
19 the :mod:`~humanfriendly.terminal` module) in the calculation of columns | |
20 widths. On reason for this is that column names are highlighted in color when | |
21 connected to a terminal. It also means that you can use ANSI escape sequences | |
22 to highlight certain column's values if you feel like it (for example to | |
23 highlight deviations from the norm in an overview of calculated values). | |
24 """ | |
25 | |
26 # Standard library modules. | |
27 import collections | |
28 import re | |
29 | |
30 # Modules included in our package. | |
31 from humanfriendly.compat import coerce_string | |
32 from humanfriendly.terminal import ( | |
33 ansi_strip, | |
34 ansi_width, | |
35 ansi_wrap, | |
36 terminal_supports_colors, | |
37 find_terminal_size, | |
38 HIGHLIGHT_COLOR, | |
39 ) | |
40 | |
41 # Public identifiers that require documentation. | |
42 __all__ = ( | |
43 'format_pretty_table', | |
44 'format_robust_table', | |
45 'format_rst_table', | |
46 'format_smart_table', | |
47 ) | |
48 | |
49 # Compiled regular expression pattern to recognize table columns containing | |
50 # numeric data (integer and/or floating point numbers). Used to right-align the | |
51 # contents of such columns. | |
52 # | |
53 # Pre-emptive snarky comment: This pattern doesn't match every possible | |
54 # floating point number notation!?!1!1 | |
55 # | |
56 # Response: I know, that's intentional. The use of this regular expression | |
57 # pattern has a very high DWIM level and weird floating point notations do not | |
58 # fall under the DWIM umbrella :-). | |
59 NUMERIC_DATA_PATTERN = re.compile(r'^\d+(\.\d+)?$') | |
60 | |
61 | |
62 def format_smart_table(data, column_names): | |
63 """ | |
64 Render tabular data using the most appropriate representation. | |
65 | |
66 :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) | |
67 containing the rows of the table, where each row is an | |
68 iterable containing the columns of the table (strings). | |
69 :param column_names: An iterable of column names (strings). | |
70 :returns: The rendered table (a string). | |
71 | |
72 If you want an easy way to render tabular data on a terminal in a human | |
73 friendly format then this function is for you! It works as follows: | |
74 | |
75 - If the input data doesn't contain any line breaks the function | |
76 :func:`format_pretty_table()` is used to render a pretty table. If the | |
77 resulting table fits in the terminal without wrapping the rendered pretty | |
78 table is returned. | |
79 | |
80 - If the input data does contain line breaks or if a pretty table would | |
81 wrap (given the width of the terminal) then the function | |
82 :func:`format_robust_table()` is used to render a more robust table that | |
83 can deal with data containing line breaks and long text. | |
84 """ | |
85 # Normalize the input in case we fall back from a pretty table to a robust | |
86 # table (in which case we'll definitely iterate the input more than once). | |
87 data = [normalize_columns(r) for r in data] | |
88 column_names = normalize_columns(column_names) | |
89 # Make sure the input data doesn't contain any line breaks (because pretty | |
90 # tables break horribly when a column's text contains a line break :-). | |
91 if not any(any('\n' in c for c in r) for r in data): | |
92 # Render a pretty table. | |
93 pretty_table = format_pretty_table(data, column_names) | |
94 # Check if the pretty table fits in the terminal. | |
95 table_width = max(map(ansi_width, pretty_table.splitlines())) | |
96 num_rows, num_columns = find_terminal_size() | |
97 if table_width <= num_columns: | |
98 # The pretty table fits in the terminal without wrapping! | |
99 return pretty_table | |
100 # Fall back to a robust table when a pretty table won't work. | |
101 return format_robust_table(data, column_names) | |
102 | |
103 | |
104 def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'): | |
105 """ | |
106 Render a table using characters like dashes and vertical bars to emulate borders. | |
107 | |
108 :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) | |
109 containing the rows of the table, where each row is an | |
110 iterable containing the columns of the table (strings). | |
111 :param column_names: An iterable of column names (strings). | |
112 :param horizontal_bar: The character used to represent a horizontal bar (a | |
113 string). | |
114 :param vertical_bar: The character used to represent a vertical bar (a | |
115 string). | |
116 :returns: The rendered table (a string). | |
117 | |
118 Here's an example: | |
119 | |
120 >>> from humanfriendly.tables import format_pretty_table | |
121 >>> column_names = ['Version', 'Uploaded on', 'Downloads'] | |
122 >>> humanfriendly_releases = [ | |
123 ... ['1.23', '2015-05-25', '218'], | |
124 ... ['1.23.1', '2015-05-26', '1354'], | |
125 ... ['1.24', '2015-05-26', '223'], | |
126 ... ['1.25', '2015-05-26', '4319'], | |
127 ... ['1.25.1', '2015-06-02', '197'], | |
128 ... ] | |
129 >>> print(format_pretty_table(humanfriendly_releases, column_names)) | |
130 ------------------------------------- | |
131 | Version | Uploaded on | Downloads | | |
132 ------------------------------------- | |
133 | 1.23 | 2015-05-25 | 218 | | |
134 | 1.23.1 | 2015-05-26 | 1354 | | |
135 | 1.24 | 2015-05-26 | 223 | | |
136 | 1.25 | 2015-05-26 | 4319 | | |
137 | 1.25.1 | 2015-06-02 | 197 | | |
138 ------------------------------------- | |
139 | |
140 Notes about the resulting table: | |
141 | |
142 - If a column contains numeric data (integer and/or floating point | |
143 numbers) in all rows (ignoring column names of course) then the content | |
144 of that column is right-aligned, as can be seen in the example above. The | |
145 idea here is to make it easier to compare the numbers in different | |
146 columns to each other. | |
147 | |
148 - The column names are highlighted in color so they stand out a bit more | |
149 (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what | |
150 that looks like (my terminals are always set to white text on a black | |
151 background): | |
152 | |
153 .. image:: images/pretty-table.png | |
154 """ | |
155 # Normalize the input because we'll have to iterate it more than once. | |
156 data = [normalize_columns(r, expandtabs=True) for r in data] | |
157 if column_names is not None: | |
158 column_names = normalize_columns(column_names) | |
159 if column_names: | |
160 if terminal_supports_colors(): | |
161 column_names = [highlight_column_name(n) for n in column_names] | |
162 data.insert(0, column_names) | |
163 # Calculate the maximum width of each column. | |
164 widths = collections.defaultdict(int) | |
165 numeric_data = collections.defaultdict(list) | |
166 for row_index, row in enumerate(data): | |
167 for column_index, column in enumerate(row): | |
168 widths[column_index] = max(widths[column_index], ansi_width(column)) | |
169 if not (column_names and row_index == 0): | |
170 numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column)))) | |
171 # Create a horizontal bar of dashes as a delimiter. | |
172 line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1) | |
173 # Start the table with a vertical bar. | |
174 lines = [line_delimiter] | |
175 # Format the rows and columns. | |
176 for row_index, row in enumerate(data): | |
177 line = [vertical_bar] | |
178 for column_index, column in enumerate(row): | |
179 padding = ' ' * (widths[column_index] - ansi_width(column)) | |
180 if all(numeric_data[column_index]): | |
181 line.append(' ' + padding + column + ' ') | |
182 else: | |
183 line.append(' ' + column + padding + ' ') | |
184 line.append(vertical_bar) | |
185 lines.append(u''.join(line)) | |
186 if column_names and row_index == 0: | |
187 lines.append(line_delimiter) | |
188 # End the table with a vertical bar. | |
189 lines.append(line_delimiter) | |
190 # Join the lines, returning a single string. | |
191 return u'\n'.join(lines) | |
192 | |
193 | |
194 def format_robust_table(data, column_names): | |
195 """ | |
196 Render tabular data with one column per line (allowing columns with line breaks). | |
197 | |
198 :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) | |
199 containing the rows of the table, where each row is an | |
200 iterable containing the columns of the table (strings). | |
201 :param column_names: An iterable of column names (strings). | |
202 :returns: The rendered table (a string). | |
203 | |
204 Here's an example: | |
205 | |
206 >>> from humanfriendly.tables import format_robust_table | |
207 >>> column_names = ['Version', 'Uploaded on', 'Downloads'] | |
208 >>> humanfriendly_releases = [ | |
209 ... ['1.23', '2015-05-25', '218'], | |
210 ... ['1.23.1', '2015-05-26', '1354'], | |
211 ... ['1.24', '2015-05-26', '223'], | |
212 ... ['1.25', '2015-05-26', '4319'], | |
213 ... ['1.25.1', '2015-06-02', '197'], | |
214 ... ] | |
215 >>> print(format_robust_table(humanfriendly_releases, column_names)) | |
216 ----------------------- | |
217 Version: 1.23 | |
218 Uploaded on: 2015-05-25 | |
219 Downloads: 218 | |
220 ----------------------- | |
221 Version: 1.23.1 | |
222 Uploaded on: 2015-05-26 | |
223 Downloads: 1354 | |
224 ----------------------- | |
225 Version: 1.24 | |
226 Uploaded on: 2015-05-26 | |
227 Downloads: 223 | |
228 ----------------------- | |
229 Version: 1.25 | |
230 Uploaded on: 2015-05-26 | |
231 Downloads: 4319 | |
232 ----------------------- | |
233 Version: 1.25.1 | |
234 Uploaded on: 2015-06-02 | |
235 Downloads: 197 | |
236 ----------------------- | |
237 | |
238 The column names are highlighted in bold font and color so they stand out a | |
239 bit more (see :data:`.HIGHLIGHT_COLOR`). | |
240 """ | |
241 blocks = [] | |
242 column_names = ["%s:" % n for n in normalize_columns(column_names)] | |
243 if terminal_supports_colors(): | |
244 column_names = [highlight_column_name(n) for n in column_names] | |
245 # Convert each row into one or more `name: value' lines (one per column) | |
246 # and group each `row of lines' into a block (i.e. rows become blocks). | |
247 for row in data: | |
248 lines = [] | |
249 for column_index, column_text in enumerate(normalize_columns(row)): | |
250 stripped_column = column_text.strip() | |
251 if '\n' not in stripped_column: | |
252 # Columns without line breaks are formatted inline. | |
253 lines.append("%s %s" % (column_names[column_index], stripped_column)) | |
254 else: | |
255 # Columns with line breaks could very well contain indented | |
256 # lines, so we'll put the column name on a separate line. This | |
257 # way any indentation remains intact, and it's easier to | |
258 # copy/paste the text. | |
259 lines.append(column_names[column_index]) | |
260 lines.extend(column_text.rstrip().splitlines()) | |
261 blocks.append(lines) | |
262 # Calculate the width of the row delimiter. | |
263 num_rows, num_columns = find_terminal_size() | |
264 longest_line = max(max(map(ansi_width, lines)) for lines in blocks) | |
265 delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns)) | |
266 # Force a delimiter at the start and end of the table. | |
267 blocks.insert(0, "") | |
268 blocks.append("") | |
269 # Embed the row delimiter between every two blocks. | |
270 return delimiter.join(u"\n".join(b) for b in blocks).strip() | |
271 | |
272 | |
273 def format_rst_table(data, column_names=None): | |
274 """ | |
275 Render a table in reStructuredText_ format. | |
276 | |
277 :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) | |
278 containing the rows of the table, where each row is an | |
279 iterable containing the columns of the table (strings). | |
280 :param column_names: An iterable of column names (strings). | |
281 :returns: The rendered table (a string). | |
282 | |
283 Here's an example: | |
284 | |
285 >>> from humanfriendly.tables import format_rst_table | |
286 >>> column_names = ['Version', 'Uploaded on', 'Downloads'] | |
287 >>> humanfriendly_releases = [ | |
288 ... ['1.23', '2015-05-25', '218'], | |
289 ... ['1.23.1', '2015-05-26', '1354'], | |
290 ... ['1.24', '2015-05-26', '223'], | |
291 ... ['1.25', '2015-05-26', '4319'], | |
292 ... ['1.25.1', '2015-06-02', '197'], | |
293 ... ] | |
294 >>> print(format_rst_table(humanfriendly_releases, column_names)) | |
295 ======= =========== ========= | |
296 Version Uploaded on Downloads | |
297 ======= =========== ========= | |
298 1.23 2015-05-25 218 | |
299 1.23.1 2015-05-26 1354 | |
300 1.24 2015-05-26 223 | |
301 1.25 2015-05-26 4319 | |
302 1.25.1 2015-06-02 197 | |
303 ======= =========== ========= | |
304 | |
305 .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText | |
306 """ | |
307 data = [normalize_columns(r) for r in data] | |
308 if column_names: | |
309 data.insert(0, normalize_columns(column_names)) | |
310 # Calculate the maximum width of each column. | |
311 widths = collections.defaultdict(int) | |
312 for row in data: | |
313 for index, column in enumerate(row): | |
314 widths[index] = max(widths[index], len(column)) | |
315 # Pad the columns using whitespace. | |
316 for row in data: | |
317 for index, column in enumerate(row): | |
318 if index < (len(row) - 1): | |
319 row[index] = column.ljust(widths[index]) | |
320 # Add table markers. | |
321 delimiter = ['=' * w for i, w in sorted(widths.items())] | |
322 if column_names: | |
323 data.insert(1, delimiter) | |
324 data.insert(0, delimiter) | |
325 data.append(delimiter) | |
326 # Join the lines and columns together. | |
327 return '\n'.join(' '.join(r) for r in data) | |
328 | |
329 | |
330 def normalize_columns(row, expandtabs=False): | |
331 results = [] | |
332 for value in row: | |
333 text = coerce_string(value) | |
334 if expandtabs: | |
335 text = text.expandtabs() | |
336 results.append(text) | |
337 return results | |
338 | |
339 | |
340 def highlight_column_name(name): | |
341 return ansi_wrap(name, bold=True, color=HIGHLIGHT_COLOR) |