flexablelib 2.0.6__cp310-cp310-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from .table import Table
2
+ from .style_compositions import TableComposition, SeparatorLine
3
+
4
+ # Names, not the objects themselves: `from flexablelib import *` walks this
5
+ # list and raises TypeError on anything that is not a string.
6
+ __all__ = ['Table', 'TableComposition', 'SeparatorLine']
flexablelib/cells.py ADDED
@@ -0,0 +1,523 @@
1
+ """ CELL WRAPPING AND ADJUSTMENT """
2
+
3
+ from .fast import pad_to_width, visible_width
4
+ from .utils import is_some_instance
5
+ from .options import FLT_FILTER, INT_FILTER
6
+
7
+ from typing import Any, List, Tuple, Union
8
+
9
+ # Columns a tab advances to. Eight is what `str.expandtabs` and every
10
+ # terminal default to.
11
+ TAB_SIZE = 8
12
+
13
+ LEFT_SIDE_WIDTH_I = 0
14
+ POINT_WIDTH_I = 1
15
+ RIGHT_SIDE_WIDTH_I = -1
16
+ LEFT_SIDE_I = 0
17
+ RIGHT_SIDE_I = 1
18
+
19
+ def _add_cell_spacing(cell: Union[str, Union[List[str], Tuple[str]]],
20
+ left: int,
21
+ right: int,
22
+ placeholder_size: int = 0,
23
+ fill_char: str = ' '
24
+ ) -> Union[str, Tuple[str]]:
25
+ """
26
+ Add spacing to a cell (string).
27
+
28
+ Examples::
29
+
30
+ _add_cell_spacing('a', 1, 3, 0) -> ' a '
31
+ _add_cell_spacing('', 1, 3, 1) -> ' '
32
+ _add_cell_spacing(['a', 'b'], 1, 3, 0) -> (' a ', ' b ')
33
+ _add_cell_spacing('', 1, 3, 0) -> ' '
34
+ """
35
+ left = fill_char * left # Space for the left
36
+ right = fill_char * right # Space for the right
37
+
38
+ # Creates a string of the desired size
39
+ # to be a placeholder when an empty string is received
40
+ # in the cell parameter.
41
+ diff = fill_char * placeholder_size
42
+
43
+ # Check if the cell is a wrapped cell (list or tuple):
44
+ if is_some_instance(cell, tuple, list):
45
+ # If it is, add the spacing to each part of the cell.
46
+ spaced = list(map(str, cell)) # Convert to strings to prevent errors.
47
+ for part_i, cell_part in enumerate(spaced):
48
+ # Add the spacing to the cell part.
49
+ # FIX should consider the cell_part length
50
+ if placeholder_size != 0 and part_i != 0:
51
+ spaced[part_i] = ''.join([left, diff, right])
52
+ else:
53
+ spaced[part_i] = ''.join([left, cell_part, right])
54
+ else:
55
+ # If it is not, add the spacing to the cell.
56
+ # FIX should consider the cell length
57
+ cell_text = str(cell)
58
+ if placeholder_size != 0:
59
+ spaced = ''.join([left, diff, right])
60
+ else:
61
+ spaced = ''.join([left, cell_text, right])
62
+
63
+ return spaced
64
+
65
+ def _center_cell(cell: Union[str, Union[List[str], Tuple[str]]],
66
+ cell_length: int,
67
+ fill_char: str = ' '
68
+ ) -> Union[str, Tuple[str]]:
69
+ """
70
+ Center a cell. Pads by visible width, so ANSI escape sequences and
71
+ double-width characters are measured correctly. Can also center
72
+ wrapped cells (lists or tuples).
73
+
74
+ Examples::
75
+
76
+ _center_cell('a', 3, '-') -> '-a-'
77
+ _center_cell(['a', 'b'], 3, '-') -> ('-a- ', '-b-')
78
+ """
79
+ # Check if the cell is a wrapped cell (list or tuple):
80
+ if is_some_instance(cell, tuple, list):
81
+ # If it is, center each part of the cell.
82
+ centered = list(map(str, cell)) # Convert to strings to prevent errors.
83
+ for part_i, cell_part in enumerate(centered):
84
+ # Center the cell part.
85
+ centered[part_i] = pad_to_width(cell_part, cell_length, 'c', fill_char)
86
+ centered = tuple(centered)
87
+ else:
88
+ # If it is not, center the cell.
89
+ centered = pad_to_width(str(cell), cell_length, 'c', fill_char)
90
+
91
+ return centered
92
+
93
+
94
+ def _ljust_cell(cell: Union[str, Union[List[str], Tuple[str]]],
95
+ cell_length: int,
96
+ fill_char: str = ' '
97
+ ) -> Union[str, Tuple[str]]:
98
+ """
99
+ Aligns a cell to the left, padding by visible width. Can also
100
+ align wrapped cells (lists or tuples).
101
+
102
+ Examples::
103
+
104
+ _ljust_cell('a', 3, '-') -> 'a--'
105
+ _ljust_cell(['a', 'b'], 3, '-') -> ('a--', 'b--')
106
+ """
107
+ # Check if the cell is a wrapped cell (list or tuple):
108
+ if is_some_instance(cell, list, tuple):
109
+ # If it is, align each part of the cell.
110
+ left_adjusted = list(map(str, cell)) # Convert to strings to prevent errors.
111
+ for part_i, cell_part in enumerate(left_adjusted):
112
+ # Align the cell part to the left.
113
+ left_adjusted[part_i] = pad_to_width(cell_part, cell_length, 'l', fill_char)
114
+ left_adjusted = tuple(left_adjusted)
115
+ else:
116
+ # If it is not, align the cell to the left.
117
+ left_adjusted = pad_to_width(str(cell), cell_length, 'l', fill_char)
118
+
119
+ return left_adjusted
120
+
121
+
122
+ def _rjust_cell(cell: Union[str, Union[List[str], Tuple[str]]],
123
+ cell_length: int,
124
+ fill_char: str = ' '
125
+ )-> Union[str, Tuple[str]]:
126
+ """
127
+ Aligns a cell to the right, padding by visible width. Can also
128
+ align wrapped cells (lists or tuples).
129
+
130
+ Examples::
131
+
132
+ _rjust_cell('a', 3, '-') -> '--a'
133
+ _rjust_cell(['a', 'b'], 3, '-') -> ('--a', '--b')
134
+ """
135
+ # Check if the cell is a wrapped cell (list or tuple):
136
+ if is_some_instance(cell, list, tuple):
137
+ # If it is, align each part of the cell.
138
+ right_adjusted = list(map(str, cell)) # Convert to strings to prevent errors.
139
+ for part_i, cell_part in enumerate(right_adjusted):
140
+ # Align the cell part to the right.
141
+ right_adjusted[part_i] = pad_to_width(cell_part, cell_length, 'r', fill_char)
142
+ right_adjusted = tuple(right_adjusted)
143
+ else:
144
+ # If it is not, align the cell to the right.
145
+ right_adjusted = pad_to_width(str(cell), cell_length, 'r', fill_char)
146
+
147
+ return right_adjusted
148
+
149
+
150
+ def fljust(string: str,
151
+ sides_widths: Union[List[int], Tuple[int, int]],
152
+ fill_char: str=' ') -> str:
153
+ """
154
+ Align a float number according to the sides widths. The purpose is
155
+ to send various float numbers to align them by the points.
156
+ The number must always have a decimal point.
157
+
158
+ Examples::
159
+
160
+ fljust('1.23', [3, 5], '-')
161
+ fljust('1.2', [3, 5], '-')
162
+ fljust('.00321', [3, 5], '-')
163
+
164
+ Results in::
165
+
166
+ '--1.23---'
167
+ '--1.2----'
168
+ '---.00321'
169
+ """
170
+ # Split at the point. ``partition`` rather than ``split`` because a
171
+ # number may not have one at all -- 1e+16 is a float and has no point,
172
+ # and indexing the second half of a one-element list raised IndexError.
173
+ left_string, point, right_string = string.partition('.')
174
+
175
+ left_width = sides_widths[LEFT_SIDE_WIDTH_I]
176
+ right_width = sides_widths[RIGHT_SIDE_WIDTH_I]
177
+
178
+ # Align the left part to the right.
179
+ left = pad_to_width(left_string, left_width, 'r', fill_char)
180
+
181
+ # Align the right part to the left.
182
+ right = pad_to_width(right_string, right_width, 'l', fill_char)
183
+
184
+ # A number with no point leaves the point's own column blank, so it is
185
+ # still exactly as wide as the ones that have one.
186
+ return ''.join([left, point or fill_char, right])
187
+
188
+
189
+ def __fljust_part(cell_part: Any,
190
+ cell_length: int,
191
+ float_column_sizes: Union[List[int], Tuple[int, int, int]],
192
+ fill_char: str = ' '
193
+ ) -> str:
194
+ """
195
+ This will align a cell part.
196
+ If it has a point, it will be aligned as a float with ``fljust()``.
197
+ Int numbers are also aligned with the int part of the floats, but
198
+ using ``_add_cell_spacing()``.
199
+ Any other element (nor float nor int) is right-aligned by visible width
200
+
201
+ Examples::
202
+
203
+ __fljust_part('1.23', 9, [3, 1, 5], '-')
204
+ __fljust_part('1.2', 9, [3, 1, 5], '-')
205
+ __fljust_part('.00321', 9, [3, 1, 5], '-')
206
+ __fljust_part('5', 9, [3, 1, 5], '-')
207
+ __fljust_part('?', 9, [3, 1, 5], '-')
208
+
209
+ Results in::
210
+
211
+ '--1.23---'
212
+ '--1.2----'
213
+ '---.00321'
214
+ '--5------'
215
+ '--------?'
216
+
217
+ """
218
+ # Convert to string because anything could come.
219
+ str_cell = str(cell_part)
220
+
221
+ # Check if the cell part is a float or an int.
222
+ is_float_number = FLT_FILTER(
223
+ str_cell
224
+ ) is not None
225
+ is_int_number = INT_FILTER(
226
+ str_cell
227
+ ) is not None
228
+
229
+ # If it is a float, align it as a float.
230
+ if is_float_number:
231
+ return fljust(
232
+ str_cell,
233
+ float_column_sizes,
234
+ fill_char
235
+ )
236
+
237
+ # If it is an int, align it as an int.
238
+ # Will be aligned to the int part of the float.
239
+ if is_int_number:
240
+ cell_len = visible_width(str_cell)
241
+
242
+ # Get the left and right sides widths, subtracting
243
+ # the length of the int part to the left and adding
244
+ # the len of the point to the right.
245
+ #
246
+ # Explanation:
247
+ # '--1.2----' Having these floats aligned.
248
+ # '---.00321'
249
+ # '--5------' If an int is going to be aligned with the same sizes:
250
+ # --- left width of 3
251
+ # - dot width of 1
252
+ # ------ right width of 5
253
+ # Will do the following:
254
+ # -- 1. Subtract its width of the left.
255
+ # ------- 2. Adding the width of the dot to the right.
256
+ # --5------- 3. add those two spaces to the left and right.
257
+
258
+ # Get the left and right sides widths.
259
+ left_width = float_column_sizes[LEFT_SIDE_WIDTH_I]
260
+ right_width = float_column_sizes[RIGHT_SIDE_WIDTH_I]
261
+
262
+ # Subtract the length of the int part to the left.
263
+ fixed_left_width = left_width - cell_len
264
+
265
+ # Add the len of the point to the right.
266
+ fixed_right_width = right_width + float_column_sizes[POINT_WIDTH_I]
267
+
268
+ # Add the spaces to the left and right.
269
+ return _add_cell_spacing(
270
+ str_cell,
271
+ fixed_left_width,
272
+ fixed_right_width,
273
+ 0,
274
+ fill_char
275
+ )
276
+
277
+ # If it is not a float nor an int, align it to the right.
278
+ else:
279
+ return pad_to_width(
280
+ str_cell, cell_length, 'r', fill_char
281
+ )
282
+
283
+
284
+ def _fljust_cell(cell: Union[str, Union[List[str], Tuple[str]]],
285
+ cell_length: int,
286
+ float_column_sizes: Union[List[int], Tuple[int, int, int]],
287
+ fill_char: str = ' ',
288
+ ) -> Union[str, Union[List[str], Tuple[str]]]:
289
+ """
290
+ Examples::
291
+
292
+ _fljust_cell('1.23', 9, [3, 1, 5], '-')
293
+ _fljust_cell('1.2', 9, [3, 1, 5], '-')
294
+ _fljust_cell('.00321', 9, [3, 1, 5], '-')
295
+ _fljust_cell('5', 9, [3, 1, 5], '-')
296
+ _fljust_cell(['Nothing', 'here'], 9, [3, 1, 5], '-')
297
+
298
+ will result in::
299
+
300
+ '--1.23---'
301
+ '--1.2----'
302
+ '---.00321'
303
+ '--5------'
304
+ ('--Nothing',
305
+ '-----here')
306
+
307
+ """
308
+ # If it is a wrapped cell (list or tuple), align each part.
309
+ if is_some_instance(cell, list, tuple):
310
+ float_adjusted = list(map(str, cell)) # Convert to string.
311
+ for part_i, cell_part in enumerate(float_adjusted):
312
+ float_adjusted[part_i] = __fljust_part(
313
+ cell_part,
314
+ cell_length,
315
+ float_column_sizes,
316
+ fill_char
317
+ )
318
+ float_adjusted = tuple(float_adjusted)
319
+
320
+ # If it is not a wrapped cell, align it.
321
+ else:
322
+ float_adjusted = __fljust_part(
323
+ cell,
324
+ cell_length,
325
+ float_column_sizes,
326
+ fill_char
327
+ )
328
+
329
+ return float_adjusted
330
+
331
+
332
+ def _zip_wrapped_rows(wrapped_headers: Union[list, tuple],
333
+ wrapped_rows: Union[list, tuple]
334
+ ) -> Tuple[tuple, tuple]:
335
+ """
336
+ Will zip a wrapped row and convert it to a wrapped column.
337
+
338
+ A wrapped row looks like this::
339
+
340
+ [['Wrapped', 'Looking']['row', 'good']]
341
+
342
+ In order to put in a column, it needs to be paired with
343
+ it's respective part of the consequent sub-rows, so 0
344
+ goes with 0, 1 goes with 1, 2 goes with 2, etc.
345
+
346
+ >>> 0 1 0 1
347
+ >>> [['Wrapped', 'Looking']['row', 'good']]
348
+
349
+ The zip function does exactly that::
350
+
351
+ tuple(zip(*[['Wrapped', 'Looking']['row', 'good']]))
352
+
353
+ results in::
354
+
355
+ (('Wrapped', 'row'), ('Looking', 'good'))
356
+
357
+ But it's also necessary to consider that the intention is to
358
+ convert a row in a column, so the process must be done twice:
359
+ one for the wrapped cells, and one for all the rows::
360
+
361
+ _zip_wrapped_rows([
362
+ [['Data' , 'In a row']], # Not wrapped.
363
+ [['Wrapped', 'Looking'] # Wrapped.
364
+ ['row' , 'good' ]],
365
+ [['More' , 'data' ]], # Not wrapped.
366
+ ])
367
+
368
+ results in::
369
+
370
+ (
371
+ ('Data', ('Wrapped', 'row'), 'More'),
372
+ ('In a row', ('Looking', 'good'), 'data'),
373
+ )
374
+ """
375
+ zipped_wrapped_cells = []
376
+ for column_i, column in enumerate(wrapped_rows):
377
+ # If there's any wrapped cells, here is where they are zipped.
378
+ zipped_wrapped_cells.append(__zip_sub_rows(column))
379
+ # Now, the rows are zipped to be converted to columns.
380
+ zipped_rows = tuple(zip(*zipped_wrapped_cells))
381
+ # The headers are zipped to be converted to columns (in case
382
+ # of a wrapped header).
383
+ zipped_headers = __zip_sub_rows(wrapped_headers)
384
+
385
+ if not zipped_rows:
386
+ # A table with columns but no rows at all still has to hand back one
387
+ # (empty) column per header. Transposing nothing yields nothing, and
388
+ # the caller then indexed past the end of it -- a table with headers
389
+ # and no data raised IndexError rather than rendering its headers.
390
+ zipped_rows = tuple(() for _ in zipped_headers)
391
+
392
+ return zipped_rows, zipped_headers
393
+
394
+
395
+ def __zip_sub_rows(row: Union[list, tuple]) -> tuple:
396
+ """
397
+ Zips a wrapped row::
398
+
399
+ __zip_sub_rows([['a', 'b'], ['c', 'd']]) -> (('a', 'c'), ('b', 'd'))
400
+ __zip_sub_rows([['a', 'b']]) -> ('a', 'b')
401
+ """
402
+ if len(row) > 1:
403
+ # If there's more than one sub-row, zip them.
404
+ zipped_sub_rows = tuple(zip(*row))
405
+ return zipped_sub_rows
406
+ else:
407
+ # If there's only one sub-row, return it.
408
+ only_sub_row = row[0]
409
+ return only_sub_row
410
+
411
+ def _wrap_rows(headers: Union[list, tuple],
412
+ data: Union[List[Union[list, tuple]], Tuple[Union[list, tuple]]]
413
+ ) -> Tuple[list, list]:
414
+ """
415
+ Wraps the rows and headers of the table if
416
+ any new line characters are present::
417
+
418
+ _wrap_rows(headers=['a', 'b\\nc'], data=[['a', 'b\\nc'], ['c', 'd']])
419
+
420
+ Results in:
421
+
422
+ >>> ([['a', 'b'], ['', 'c']], [[['a', 'b'], ['', 'c']], [['c', 'd']]])
423
+ >>> | ^ ^ | | ^ ^ | | |
424
+ >>> # sub-row sub-row sub-row sub-row no sub-rows
425
+ >>> #<-------wrapped-------> <-------wrapped-------> <not wrapped>
426
+ """
427
+ # If no headers are provided, return None.
428
+ wrapped_headers = None
429
+
430
+ there_are_headers = headers is not None and is_some_instance(headers, list, tuple)
431
+ if there_are_headers:
432
+ # If there are headers, wrap them.
433
+ wrapped_headers = __wrap_single_row(headers)
434
+
435
+ # Wrap each row.
436
+ # If no new line characters are present, data is returned as it is.
437
+ wrapped_data = list(map(__wrap_single_row, data))
438
+
439
+ return wrapped_headers, wrapped_data
440
+
441
+
442
+ def __wrap_single_row(row: Union[list, tuple]) -> list:
443
+ """
444
+ Will split every cell that has an new line character.
445
+ The splitted cell generates a new sub-row, and every other
446
+ cell that doesn't have a new line character will be adjusted::
447
+
448
+ __wrap_single_row(['a', 'b\\nc'])
449
+
450
+ Results in:
451
+
452
+ >>> [['a', 'b'], ['', 'c']]
453
+ >>> ^
454
+ >>> # Blank space to adjust the cell.
455
+ """
456
+ # Split in lines if any new line characters are present.
457
+ splitted_cells = list(map(__wrap_cell, row))
458
+
459
+ # Get the quantity of sub-rows calculating the max length of
460
+ # all of the splitted (or not) cells.
461
+ quantity_of_sub_rows = max([len(x) for x in splitted_cells])
462
+
463
+ # Create blank sub-rows each one having the same quantity of
464
+ # cells as the original row.
465
+ sub_rows = [['' for x in range(len(row))]
466
+ for _ in range(quantity_of_sub_rows)]
467
+
468
+ # Fill the sub-rows with the splitted cells.
469
+ #
470
+ # [['a'], ['b', 'c']] <-- splitted_cells
471
+ # ^ ^ ^
472
+ # sbr0 sbr0 sbr1
473
+ # <cell0> <--cell1-->
474
+ #
475
+ # [['a', 'b'], ['', 'c']] <-- sub_rows (being filled)
476
+ # ^ ^ ^ ^
477
+ # [0][0] [0][1] ^ [1][1] <-- sub_rows[sub_row][cell]
478
+ # [0][0] [1][0] ^ [1][1] <-- splitted_cells[cell][sub-row]
479
+ # ^
480
+ # There's no second sub-row in the first cell,
481
+ # so it's just left empty
482
+ #
483
+ for cell in range(len(splitted_cells)):
484
+ for sub_row in range(len(splitted_cells[cell])):
485
+ sub_rows[sub_row][cell] = splitted_cells[cell][sub_row]
486
+
487
+ return sub_rows
488
+
489
+
490
+ def __wrap_cell(cell: Any) -> Union[List[str], Any]:
491
+ """
492
+ Split in lines a cell if it has a new line character::
493
+
494
+ 'a' -> ['a']
495
+ 'a\\nb' -> ['a', 'b']
496
+ 'a\\tb' -> ['a b']
497
+ '' -> ['']
498
+ 123 -> [123]
499
+ None -> [None]
500
+
501
+ Tabs are expanded here, because a tab has no width of its own -- it
502
+ means "advance to the next stop", and where that stop is depends on a
503
+ console this string may never reach. Left alone it measured as nothing
504
+ and then displaced the border by however far the terminal moved the
505
+ cursor. Expanding against the start of the cell gives a width that is
506
+ the same everywhere the table is read.
507
+ """
508
+ # Only if the cell is a string.
509
+ if isinstance(cell, str):
510
+ if cell != '':
511
+ # If the cell is not empty,
512
+ # apply the str method splitlines
513
+ return cell.expandtabs(TAB_SIZE).splitlines()
514
+ else:
515
+ # Return a list with empty string in case of empty cell.
516
+ return ['']
517
+ else:
518
+ # If the cell is not a string, return it as it is.
519
+ return [cell]
520
+
521
+
522
+ if __name__ == '__main__':
523
+ print('This shouldn\'t be printing.')