brainstate 0.1.0.post20250212__py2.py3-none-any.whl → 0.1.0.post20250217__py2.py3-none-any.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.
Files changed (97) hide show
  1. brainstate/_state.py +853 -90
  2. brainstate/_state_test.py +1 -3
  3. brainstate/augment/__init__.py +2 -2
  4. brainstate/augment/_autograd.py +257 -115
  5. brainstate/augment/_autograd_test.py +2 -3
  6. brainstate/augment/_eval_shape.py +3 -4
  7. brainstate/augment/_mapping.py +582 -62
  8. brainstate/augment/_mapping_test.py +114 -30
  9. brainstate/augment/_random.py +61 -7
  10. brainstate/compile/_ad_checkpoint.py +2 -3
  11. brainstate/compile/_conditions.py +4 -5
  12. brainstate/compile/_conditions_test.py +1 -2
  13. brainstate/compile/_error_if.py +1 -2
  14. brainstate/compile/_error_if_test.py +1 -2
  15. brainstate/compile/_jit.py +23 -16
  16. brainstate/compile/_jit_test.py +1 -2
  17. brainstate/compile/_loop_collect_return.py +18 -10
  18. brainstate/compile/_loop_collect_return_test.py +1 -1
  19. brainstate/compile/_loop_no_collection.py +5 -5
  20. brainstate/compile/_make_jaxpr.py +23 -21
  21. brainstate/compile/_make_jaxpr_test.py +1 -2
  22. brainstate/compile/_progress_bar.py +1 -2
  23. brainstate/compile/_unvmap.py +1 -0
  24. brainstate/compile/_util.py +4 -2
  25. brainstate/environ.py +4 -4
  26. brainstate/environ_test.py +1 -2
  27. brainstate/functional/_activations.py +1 -2
  28. brainstate/functional/_activations_test.py +1 -1
  29. brainstate/functional/_normalization.py +1 -2
  30. brainstate/functional/_others.py +1 -2
  31. brainstate/functional/_spikes.py +136 -20
  32. brainstate/graph/_graph_node.py +2 -43
  33. brainstate/graph/_graph_operation.py +4 -20
  34. brainstate/graph/_graph_operation_test.py +3 -4
  35. brainstate/init/_base.py +1 -2
  36. brainstate/init/_generic.py +1 -2
  37. brainstate/nn/__init__.py +8 -0
  38. brainstate/nn/_collective_ops.py +351 -48
  39. brainstate/nn/_collective_ops_test.py +36 -0
  40. brainstate/nn/_common.py +193 -0
  41. brainstate/nn/_dyn_impl/_dynamics_neuron.py +1 -2
  42. brainstate/nn/_dyn_impl/_dynamics_neuron_test.py +1 -2
  43. brainstate/nn/_dyn_impl/_dynamics_synapse.py +1 -2
  44. brainstate/nn/_dyn_impl/_dynamics_synapse_test.py +1 -2
  45. brainstate/nn/_dyn_impl/_inputs.py +1 -2
  46. brainstate/nn/_dyn_impl/_rate_rnns.py +1 -2
  47. brainstate/nn/_dyn_impl/_rate_rnns_test.py +1 -2
  48. brainstate/nn/_dyn_impl/_readout.py +2 -3
  49. brainstate/nn/_dyn_impl/_readout_test.py +1 -2
  50. brainstate/nn/_dynamics/_dynamics_base.py +6 -1
  51. brainstate/nn/_dynamics/_dynamics_base_test.py +1 -2
  52. brainstate/nn/_dynamics/_state_delay.py +3 -3
  53. brainstate/nn/_dynamics/_synouts_test.py +1 -2
  54. brainstate/nn/_elementwise/_dropout.py +6 -7
  55. brainstate/nn/_elementwise/_dropout_test.py +1 -2
  56. brainstate/nn/_elementwise/_elementwise.py +1 -2
  57. brainstate/nn/_exp_euler.py +1 -2
  58. brainstate/nn/_exp_euler_test.py +1 -2
  59. brainstate/nn/_interaction/_conv.py +1 -2
  60. brainstate/nn/_interaction/_conv_test.py +1 -0
  61. brainstate/nn/_interaction/_linear.py +1 -2
  62. brainstate/nn/_interaction/_linear_test.py +1 -2
  63. brainstate/nn/_interaction/_normalizations.py +1 -2
  64. brainstate/nn/_interaction/_poolings.py +3 -4
  65. brainstate/nn/_module.py +68 -19
  66. brainstate/nn/_module_test.py +1 -2
  67. brainstate/nn/_utils.py +89 -0
  68. brainstate/nn/metrics.py +3 -4
  69. brainstate/optim/_lr_scheduler.py +1 -2
  70. brainstate/optim/_lr_scheduler_test.py +2 -3
  71. brainstate/optim/_optax_optimizer_test.py +1 -2
  72. brainstate/optim/_sgd_optimizer.py +2 -3
  73. brainstate/random/_rand_funs.py +1 -2
  74. brainstate/random/_rand_funs_test.py +2 -3
  75. brainstate/random/_rand_seed.py +2 -3
  76. brainstate/random/_rand_seed_test.py +1 -2
  77. brainstate/random/_rand_state.py +3 -4
  78. brainstate/surrogate.py +5 -5
  79. brainstate/transform.py +0 -3
  80. brainstate/typing.py +28 -25
  81. brainstate/util/__init__.py +9 -7
  82. brainstate/util/_caller.py +1 -2
  83. brainstate/util/_error.py +27 -0
  84. brainstate/util/_others.py +60 -15
  85. brainstate/util/{_dict.py → _pretty_pytree.py} +2 -2
  86. brainstate/util/{_dict_test.py → _pretty_pytree_test.py} +1 -2
  87. brainstate/util/_pretty_repr.py +1 -2
  88. brainstate/util/_pretty_table.py +2900 -0
  89. brainstate/util/_struct.py +11 -11
  90. brainstate/util/filter.py +472 -0
  91. {brainstate-0.1.0.post20250212.dist-info → brainstate-0.1.0.post20250217.dist-info}/METADATA +2 -2
  92. brainstate-0.1.0.post20250217.dist-info/RECORD +128 -0
  93. brainstate/util/_filter.py +0 -178
  94. brainstate-0.1.0.post20250212.dist-info/RECORD +0 -124
  95. {brainstate-0.1.0.post20250212.dist-info → brainstate-0.1.0.post20250217.dist-info}/LICENSE +0 -0
  96. {brainstate-0.1.0.post20250212.dist-info → brainstate-0.1.0.post20250217.dist-info}/WHEEL +0 -0
  97. {brainstate-0.1.0.post20250212.dist-info → brainstate-0.1.0.post20250217.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2900 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Copyright (c) 2009-2014, Luke Maurits <luke@maurits.id.au>
4
+ # All rights reserved.
5
+ # With contributions from:
6
+ # * Chris Clark
7
+ # * Klein Stephane
8
+ # * John Filleau
9
+ # * Vladimir Vrzić
10
+ #
11
+ # Redistribution and use in source and binary forms, with or without
12
+ # modification, are permitted provided that the following conditions are met:
13
+ #
14
+ # * Redistributions of source code must retain the above copyright notice,
15
+ # this list of conditions and the following disclaimer.
16
+ # * Redistributions in binary form must reproduce the above copyright notice,
17
+ # this list of conditions and the following disclaimer in the documentation
18
+ # and/or other materials provided with the distribution.
19
+ # * The name of the author may not be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+
34
+ from __future__ import annotations
35
+
36
+ import io
37
+ import re
38
+ import warnings
39
+ from collections.abc import Callable, Iterable, Mapping, Sequence
40
+ from enum import IntEnum
41
+ from html.parser import HTMLParser
42
+ from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, cast
43
+
44
+ if TYPE_CHECKING:
45
+ from sqlite3 import Cursor
46
+
47
+ from _typeshed import SupportsRichComparison
48
+ from typing_extensions import Self, TypeAlias
49
+
50
+ __all__ = [
51
+ "PrettyTable"
52
+ ]
53
+
54
+
55
+ class HRuleStyle(IntEnum):
56
+ FRAME = 0
57
+ ALL = 1
58
+ NONE = 2
59
+ HEADER = 3
60
+
61
+
62
+ class VRuleStyle(IntEnum):
63
+ FRAME = 0
64
+ ALL = 1
65
+ NONE = 2
66
+
67
+
68
+ class TableStyle(IntEnum):
69
+ DEFAULT = 10
70
+ MSWORD_FRIENDLY = 11
71
+ PLAIN_COLUMNS = 12
72
+ MARKDOWN = 13
73
+ ORGMODE = 14
74
+ DOUBLE_BORDER = 15
75
+ SINGLE_BORDER = 16
76
+ RANDOM = 20
77
+
78
+
79
+ # keep for backwards compatibility
80
+ _DEPRECATED_FRAME: Final = 0
81
+ _DEPRECATED_ALL: Final = 1
82
+ _DEPRECATED_NONE: Final = 2
83
+ _DEPRECATED_HEADER: Final = 3
84
+ _DEPRECATED_DEFAULT: Final = TableStyle.DEFAULT
85
+ _DEPRECATED_MSWORD_FRIENDLY: Final = TableStyle.MSWORD_FRIENDLY
86
+ _DEPRECATED_PLAIN_COLUMNS: Final = TableStyle.PLAIN_COLUMNS
87
+ _DEPRECATED_MARKDOWN: Final = TableStyle.MARKDOWN
88
+ _DEPRECATED_ORGMODE: Final = TableStyle.ORGMODE
89
+ _DEPRECATED_DOUBLE_BORDER: Final = TableStyle.DOUBLE_BORDER
90
+ _DEPRECATED_SINGLE_BORDER: Final = TableStyle.SINGLE_BORDER
91
+ _DEPRECATED_RANDOM: Final = TableStyle.RANDOM
92
+ # --------------------------------
93
+
94
+ BASE_ALIGN_VALUE: Final = "base_align_value"
95
+
96
+ RowType: TypeAlias = list[Any]
97
+ AlignType: TypeAlias = Literal["l", "c", "r"]
98
+ VAlignType: TypeAlias = Literal["t", "m", "b"]
99
+ HeaderStyleType: TypeAlias = Literal["cap", "title", "upper", "lower", None]
100
+
101
+
102
+ class OptionsType(TypedDict):
103
+ title: str | None
104
+ start: int
105
+ end: int | None
106
+ fields: Sequence[str | None] | None
107
+ header: bool
108
+ border: bool
109
+ preserve_internal_border: bool
110
+ sortby: str | None
111
+ reversesort: bool
112
+ sort_key: Callable[[RowType], SupportsRichComparison]
113
+ attributes: dict[str, str]
114
+ format: bool
115
+ hrules: HRuleStyle
116
+ vrules: VRuleStyle
117
+ int_format: str | dict[str, str] | None
118
+ float_format: str | dict[str, str] | None
119
+ custom_format: (
120
+ Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
121
+ )
122
+ min_table_width: int | None
123
+ max_table_width: int | None
124
+ padding_width: int
125
+ left_padding_width: int | None
126
+ right_padding_width: int | None
127
+ vertical_char: str
128
+ horizontal_char: str
129
+ horizontal_align_char: str
130
+ junction_char: str
131
+ header_style: HeaderStyleType
132
+ xhtml: bool
133
+ print_empty: bool
134
+ oldsortslice: bool
135
+ top_junction_char: str
136
+ bottom_junction_char: str
137
+ right_junction_char: str
138
+ left_junction_char: str
139
+ top_right_junction_char: str
140
+ top_left_junction_char: str
141
+ bottom_right_junction_char: str
142
+ bottom_left_junction_char: str
143
+ align: dict[str, AlignType]
144
+ valign: dict[str, VAlignType]
145
+ min_width: int | dict[str, int] | None
146
+ max_width: int | dict[str, int] | None
147
+ none_format: str | dict[str, str | None] | None
148
+ escape_header: bool
149
+ escape_data: bool
150
+
151
+
152
+ _re = re.compile(r"\033\[[0-9;]*m|\033\(B")
153
+
154
+
155
+ def _get_size(text: str) -> tuple[int, int]:
156
+ lines = text.split("\n")
157
+ height = len(lines)
158
+ width = max(_str_block_width(line) for line in lines)
159
+ return width, height
160
+
161
+
162
+ class PrettyTable:
163
+ __module__ = 'brainstate.util'
164
+
165
+ _xhtml: bool
166
+ _align: dict[str, AlignType]
167
+ _valign: dict[str, VAlignType]
168
+ _min_width: dict[str, int]
169
+ _max_width: dict[str, int]
170
+ _min_table_width: int | None
171
+ _max_table_width: int | None
172
+ _fields: Sequence[str | None] | None
173
+ _title: str | None
174
+ _start: int
175
+ _end: int | None
176
+ _sortby: str | None
177
+ _reversesort: bool
178
+ _sort_key: Callable[[RowType], SupportsRichComparison]
179
+ _header: bool
180
+ _header_style: HeaderStyleType
181
+ _border: bool
182
+ _preserve_internal_border: bool
183
+ _hrules: HRuleStyle
184
+ _vrules: VRuleStyle
185
+ _int_format: dict[str, str]
186
+ _float_format: dict[str, str]
187
+ _custom_format: dict[str, Callable[[str, Any], str]]
188
+ _padding_width: int
189
+ _left_padding_width: int | None
190
+ _right_padding_width: int | None
191
+ _vertical_char: str
192
+ _horizontal_char: str
193
+ _horizontal_align_char: str | None
194
+ _junction_char: str
195
+ _top_junction_char: str | None
196
+ _bottom_junction_char: str | None
197
+ _right_junction_char: str | None
198
+ _left_junction_char: str | None
199
+ _top_right_junction_char: str | None
200
+ _top_left_junction_char: str | None
201
+ _bottom_right_junction_char: str | None
202
+ _bottom_left_junction_char: str | None
203
+ _format: bool
204
+ _print_empty: bool
205
+ _oldsortslice: bool
206
+ _attributes: dict[str, str]
207
+ _escape_header: bool
208
+ _escape_data: bool
209
+ _style: TableStyle | None
210
+ orgmode: bool
211
+ _widths: list[int]
212
+ _hrule: str
213
+
214
+ def __init__(self, field_names: Sequence[str] | None = None, **kwargs) -> None:
215
+ """Return a new PrettyTable instance
216
+
217
+ Arguments:
218
+
219
+ encoding - Unicode encoding scheme used to decode any encoded input
220
+ title - optional table title
221
+ field_names - list or tuple of field names
222
+ fields - list or tuple of field names to include in displays
223
+ start - index of first data row to include in output
224
+ end - index of last data row to include in output PLUS ONE (list slice style)
225
+ header - print a header showing field names (True or False)
226
+ header_style - stylisation to apply to field names in header
227
+ ("cap", "title", "upper", "lower" or None)
228
+ border - print a border around the table (True or False)
229
+ preserve_internal_border - print a border inside the table even if
230
+ border is disabled (True or False)
231
+ hrules - controls printing of horizontal rules after rows.
232
+ Allowed values: HRuleStyle
233
+ vrules - controls printing of vertical rules between columns.
234
+ Allowed values: VRuleStyle
235
+ int_format - controls formatting of integer data
236
+ float_format - controls formatting of floating point data
237
+ custom_format - controls formatting of any column using callable
238
+ min_table_width - minimum desired table width, in characters
239
+ max_table_width - maximum desired table width, in characters
240
+ min_width - minimum desired field width, in characters
241
+ max_width - maximum desired field width, in characters
242
+ padding_width - number of spaces on either side of column data
243
+ (only used if left and right paddings are None)
244
+ left_padding_width - number of spaces on left hand side of column data
245
+ right_padding_width - number of spaces on right hand side of column data
246
+ vertical_char - single character string used to draw vertical lines
247
+ horizontal_char - single character string used to draw horizontal lines
248
+ horizontal_align_char - single character string used to indicate alignment
249
+ junction_char - single character string used to draw line junctions
250
+ top_junction_char - single character string used to draw top line junctions
251
+ bottom_junction_char -
252
+ single character string used to draw bottom line junctions
253
+ right_junction_char - single character string used to draw right line junctions
254
+ left_junction_char - single character string used to draw left line junctions
255
+ top_right_junction_char -
256
+ single character string used to draw top-right line junctions
257
+ top_left_junction_char -
258
+ single character string used to draw top-left line junctions
259
+ bottom_right_junction_char -
260
+ single character string used to draw bottom-right line junctions
261
+ bottom_left_junction_char -
262
+ single character string used to draw bottom-left line junctions
263
+ sortby - name of field to sort rows by
264
+ sort_key - sorting key function, applied to data points before sorting
265
+ align - default align for each column (None, "l", "c" or "r")
266
+ valign - default valign for each row (None, "t", "m" or "b")
267
+ reversesort - True or False to sort in descending or ascending order
268
+ oldsortslice - Slice rows before sorting in the "old style" """
269
+
270
+ self.encoding = kwargs.get("encoding", "UTF-8")
271
+
272
+ # Data
273
+ self._field_names: list[str] = []
274
+ self._rows: list[RowType] = []
275
+ self._dividers: list[bool] = []
276
+ self.align = {}
277
+ self.valign = {}
278
+ self.max_width = {}
279
+ self.min_width = {}
280
+ self.int_format = {}
281
+ self.float_format = {}
282
+ self.custom_format = {}
283
+ self._style = None
284
+
285
+ # Options
286
+ self._options = [
287
+ "title",
288
+ "start",
289
+ "end",
290
+ "fields",
291
+ "header",
292
+ "border",
293
+ "preserve_internal_border",
294
+ "sortby",
295
+ "reversesort",
296
+ "sort_key",
297
+ "attributes",
298
+ "format",
299
+ "hrules",
300
+ "vrules",
301
+ "int_format",
302
+ "float_format",
303
+ "custom_format",
304
+ "min_table_width",
305
+ "max_table_width",
306
+ "padding_width",
307
+ "left_padding_width",
308
+ "right_padding_width",
309
+ "vertical_char",
310
+ "horizontal_char",
311
+ "horizontal_align_char",
312
+ "junction_char",
313
+ "header_style",
314
+ "xhtml",
315
+ "print_empty",
316
+ "oldsortslice",
317
+ "top_junction_char",
318
+ "bottom_junction_char",
319
+ "right_junction_char",
320
+ "left_junction_char",
321
+ "top_right_junction_char",
322
+ "top_left_junction_char",
323
+ "bottom_right_junction_char",
324
+ "bottom_left_junction_char",
325
+ "align",
326
+ "valign",
327
+ "max_width",
328
+ "min_width",
329
+ "none_format",
330
+ "escape_header",
331
+ "escape_data",
332
+ ]
333
+
334
+ self._none_format: dict[str, str | None] = {}
335
+ self._kwargs = {}
336
+ if field_names:
337
+ self.field_names = field_names
338
+ else:
339
+ self._widths: list[int] = []
340
+
341
+ for option in self._options:
342
+ if option in kwargs:
343
+ self._validate_option(option, kwargs[option])
344
+ self._kwargs[option] = kwargs[option]
345
+ else:
346
+ kwargs[option] = None
347
+ self._kwargs[option] = None
348
+
349
+ self._title = kwargs["title"] or None
350
+ self._start = kwargs["start"] or 0
351
+ self._end = kwargs["end"] or None
352
+ self._fields = kwargs["fields"] or None
353
+
354
+ if kwargs["header"] in (True, False):
355
+ self._header = kwargs["header"]
356
+ else:
357
+ self._header = True
358
+ self._header_style = kwargs["header_style"] or None
359
+ if kwargs["border"] in (True, False):
360
+ self._border = kwargs["border"]
361
+ else:
362
+ self._border = True
363
+ if kwargs["preserve_internal_border"] in (True, False):
364
+ self._preserve_internal_border = kwargs["preserve_internal_border"]
365
+ else:
366
+ self._preserve_internal_border = False
367
+ self._hrules = kwargs["hrules"] or HRuleStyle.FRAME
368
+ self._vrules = kwargs["vrules"] or VRuleStyle.ALL
369
+
370
+ self._sortby = kwargs["sortby"] or None
371
+ if kwargs["reversesort"] in (True, False):
372
+ self._reversesort = kwargs["reversesort"]
373
+ else:
374
+ self._reversesort = False
375
+ self._sort_key = kwargs["sort_key"] or (lambda x: x)
376
+
377
+ if kwargs["escape_data"] in (True, False):
378
+ self._escape_data = kwargs["escape_data"]
379
+ else:
380
+ self._escape_data = True
381
+ if kwargs["escape_header"] in (True, False):
382
+ self._escape_header = kwargs["escape_header"]
383
+ else:
384
+ self._escape_header = True
385
+
386
+ self._column_specific_args()
387
+
388
+ self._min_table_width = kwargs["min_table_width"] or None
389
+ self._max_table_width = kwargs["max_table_width"] or None
390
+ if kwargs["padding_width"] is None:
391
+ self._padding_width = 1
392
+ else:
393
+ self._padding_width = kwargs["padding_width"]
394
+ self._left_padding_width = kwargs["left_padding_width"] or None
395
+ self._right_padding_width = kwargs["right_padding_width"] or None
396
+
397
+ self._vertical_char = kwargs["vertical_char"] or "|"
398
+ self._horizontal_char = kwargs["horizontal_char"] or "-"
399
+ self._horizontal_align_char = kwargs["horizontal_align_char"]
400
+ self._junction_char = kwargs["junction_char"] or "+"
401
+ self._top_junction_char = kwargs["top_junction_char"]
402
+ self._bottom_junction_char = kwargs["bottom_junction_char"]
403
+ self._right_junction_char = kwargs["right_junction_char"]
404
+ self._left_junction_char = kwargs["left_junction_char"]
405
+ self._top_right_junction_char = kwargs["top_right_junction_char"]
406
+ self._top_left_junction_char = kwargs["top_left_junction_char"]
407
+ self._bottom_right_junction_char = kwargs["bottom_right_junction_char"]
408
+ self._bottom_left_junction_char = kwargs["bottom_left_junction_char"]
409
+
410
+ if kwargs["print_empty"] in (True, False):
411
+ self._print_empty = kwargs["print_empty"]
412
+ else:
413
+ self._print_empty = True
414
+ if kwargs["oldsortslice"] in (True, False):
415
+ self._oldsortslice = kwargs["oldsortslice"]
416
+ else:
417
+ self._oldsortslice = False
418
+ self._format = kwargs["format"] or False
419
+ self._xhtml = kwargs["xhtml"] or False
420
+ self._attributes = kwargs["attributes"] or {}
421
+
422
+ def _column_specific_args(self):
423
+ # Column specific arguments, use property.setters
424
+ for attr in (
425
+ "align",
426
+ "valign",
427
+ "max_width",
428
+ "min_width",
429
+ "int_format",
430
+ "float_format",
431
+ "custom_format",
432
+ "none_format",
433
+ ):
434
+ setattr(
435
+ self, attr, (self._kwargs[attr] or {}) if attr in self._kwargs else {}
436
+ )
437
+
438
+ def _justify(self, text: str, width: int, align: AlignType) -> str:
439
+ excess = width - _str_block_width(text)
440
+ if align == "l":
441
+ return text + excess * " "
442
+ elif align == "r":
443
+ return excess * " " + text
444
+ else:
445
+ if excess % 2:
446
+ # Uneven padding
447
+ # Put more space on right if text is of odd length...
448
+ if _str_block_width(text) % 2:
449
+ return (excess // 2) * " " + text + (excess // 2 + 1) * " "
450
+ # and more space on left if text is of even length
451
+ else:
452
+ return (excess // 2 + 1) * " " + text + (excess // 2) * " "
453
+ # Why distribute extra space this way? To match the behaviour of
454
+ # the inbuilt str.center() method.
455
+ else:
456
+ # Equal padding on either side
457
+ return (excess // 2) * " " + text + (excess // 2) * " "
458
+
459
+ def __getattr__(self, name):
460
+ if name == "rowcount":
461
+ return len(self._rows)
462
+ elif name == "colcount":
463
+ if self._field_names:
464
+ return len(self._field_names)
465
+ elif self._rows:
466
+ return len(self._rows[0])
467
+ else:
468
+ return 0
469
+ else:
470
+ raise AttributeError(name)
471
+
472
+ def __getitem__(self, index: int | slice) -> PrettyTable:
473
+ new = PrettyTable()
474
+ new.field_names = self.field_names
475
+ for attr in self._options:
476
+ setattr(new, "_" + attr, getattr(self, "_" + attr))
477
+ setattr(new, "_align", getattr(self, "_align"))
478
+ if isinstance(index, slice):
479
+ for row in self._rows[index]:
480
+ new.add_row(row)
481
+ elif isinstance(index, int):
482
+ new.add_row(self._rows[index])
483
+ else:
484
+ msg = f"Index {index} is invalid, must be an integer or slice"
485
+ raise IndexError(msg)
486
+ return new
487
+
488
+ def __str__(self) -> str:
489
+ return self.get_string()
490
+
491
+ def __repr__(self) -> str:
492
+ return self.get_string()
493
+
494
+ def _repr_html_(self) -> str:
495
+ """
496
+ Returns get_html_string value by default
497
+ as the repr call in Jupyter notebook environment
498
+ """
499
+ return self.get_html_string()
500
+
501
+ ##############################
502
+ # ATTRIBUTE VALIDATORS #
503
+ ##############################
504
+
505
+ # The method _validate_option is all that should be used elsewhere in the code base
506
+ # to validate options. It will call the appropriate validation method for that
507
+ # option. The individual validation methods should never need to be called directly
508
+ # (although nothing bad will happen if they *are*).
509
+ # Validation happens in TWO places.
510
+ # Firstly, in the property setters defined in the ATTRIBUTE MANAGEMENT section.
511
+ # Secondly, in the _get_options method, where keyword arguments are mixed with
512
+ # persistent settings
513
+
514
+ def _validate_option(self, option, val) -> None:
515
+ if option == "field_names":
516
+ self._validate_field_names(val)
517
+ elif option == "none_format":
518
+ self._validate_none_format(val)
519
+ elif option in (
520
+ "start",
521
+ "end",
522
+ "max_width",
523
+ "min_width",
524
+ "min_table_width",
525
+ "max_table_width",
526
+ "padding_width",
527
+ "left_padding_width",
528
+ "right_padding_width",
529
+ ):
530
+ self._validate_nonnegative_int(option, val)
531
+ elif option == "sortby":
532
+ self._validate_field_name(option, val)
533
+ elif option == "sort_key":
534
+ self._validate_function(option, val)
535
+ elif option == "hrules":
536
+ self._validate_hrules(option, val)
537
+ elif option == "vrules":
538
+ self._validate_vrules(option, val)
539
+ elif option == "fields":
540
+ self._validate_all_field_names(option, val)
541
+ elif option in (
542
+ "header",
543
+ "border",
544
+ "preserve_internal_border",
545
+ "reversesort",
546
+ "xhtml",
547
+ "format",
548
+ "print_empty",
549
+ "oldsortslice",
550
+ "escape_header",
551
+ "escape_data",
552
+ ):
553
+ self._validate_true_or_false(option, val)
554
+ elif option == "header_style":
555
+ self._validate_header_style(val)
556
+ elif option == "int_format":
557
+ self._validate_int_format(option, val)
558
+ elif option == "float_format":
559
+ self._validate_float_format(option, val)
560
+ elif option == "custom_format":
561
+ for k, formatter in val.items():
562
+ self._validate_function(f"{option}.{k}", formatter)
563
+ elif option in (
564
+ "vertical_char",
565
+ "horizontal_char",
566
+ "horizontal_align_char",
567
+ "junction_char",
568
+ "top_junction_char",
569
+ "bottom_junction_char",
570
+ "right_junction_char",
571
+ "left_junction_char",
572
+ "top_right_junction_char",
573
+ "top_left_junction_char",
574
+ "bottom_right_junction_char",
575
+ "bottom_left_junction_char",
576
+ ):
577
+ self._validate_single_char(option, val)
578
+ elif option == "attributes":
579
+ self._validate_attributes(option, val)
580
+
581
+ def _validate_field_names(self, val):
582
+ # Check for appropriate length
583
+ if self._field_names:
584
+ try:
585
+ assert len(val) == len(self._field_names)
586
+ except AssertionError:
587
+ msg = (
588
+ "Field name list has incorrect number of values, "
589
+ f"(actual) {len(val)}!={len(self._field_names)} (expected)"
590
+ )
591
+ raise ValueError(msg)
592
+ if self._rows:
593
+ try:
594
+ assert len(val) == len(self._rows[0])
595
+ except AssertionError:
596
+ msg = (
597
+ "Field name list has incorrect number of values, "
598
+ f"(actual) {len(val)}!={len(self._rows[0])} (expected)"
599
+ )
600
+ raise ValueError(msg)
601
+ # Check for uniqueness
602
+ try:
603
+ assert len(val) == len(set(val))
604
+ except AssertionError:
605
+ msg = "Field names must be unique"
606
+ raise ValueError(msg)
607
+
608
+ def _validate_none_format(self, val):
609
+ try:
610
+ if val is not None:
611
+ assert isinstance(val, str)
612
+ except AssertionError:
613
+ msg = "Replacement for None value must be a string if being supplied."
614
+ raise TypeError(msg)
615
+
616
+ def _validate_header_style(self, val):
617
+ try:
618
+ assert val in ("cap", "title", "upper", "lower", None)
619
+ except AssertionError:
620
+ msg = "Invalid header style, use cap, title, upper, lower or None"
621
+ raise ValueError(msg)
622
+
623
+ def _validate_align(self, val):
624
+ try:
625
+ assert val in ["l", "c", "r"]
626
+ except AssertionError:
627
+ msg = f"Alignment {val} is invalid, use l, c or r"
628
+ raise ValueError(msg)
629
+
630
+ def _validate_valign(self, val):
631
+ try:
632
+ assert val in ["t", "m", "b"]
633
+ except AssertionError:
634
+ msg = f"Alignment {val} is invalid, use t, m, b"
635
+ raise ValueError(msg)
636
+
637
+ def _validate_nonnegative_int(self, name, val):
638
+ try:
639
+ assert int(val) >= 0
640
+ except AssertionError:
641
+ msg = f"Invalid value for {name}: {val}"
642
+ raise ValueError(msg)
643
+
644
+ def _validate_true_or_false(self, name, val):
645
+ try:
646
+ assert val in (True, False)
647
+ except AssertionError:
648
+ msg = f"Invalid value for {name}. Must be True or False."
649
+ raise ValueError(msg)
650
+
651
+ def _validate_int_format(self, name, val):
652
+ if val == "":
653
+ return
654
+ try:
655
+ assert isinstance(val, str)
656
+ assert val.isdigit()
657
+ except AssertionError:
658
+ msg = f"Invalid value for {name}. Must be an integer format string."
659
+ raise ValueError(msg)
660
+
661
+ def _validate_float_format(self, name, val):
662
+ if val == "":
663
+ return
664
+ try:
665
+ assert isinstance(val, str)
666
+ assert "." in val
667
+ bits = val.split(".")
668
+ assert len(bits) <= 2
669
+ assert bits[0] == "" or bits[0].isdigit()
670
+ assert (
671
+ bits[1] == ""
672
+ or bits[1].isdigit()
673
+ or (bits[1][-1] == "f" and bits[1].rstrip("f").isdigit())
674
+ )
675
+ except AssertionError:
676
+ msg = f"Invalid value for {name}. Must be a float format string."
677
+ raise ValueError(msg)
678
+
679
+ def _validate_function(self, name, val):
680
+ try:
681
+ assert hasattr(val, "__call__")
682
+ except AssertionError:
683
+ msg = f"Invalid value for {name}. Must be a function."
684
+ raise ValueError(msg)
685
+
686
+ def _validate_hrules(self, name, val):
687
+ try:
688
+ assert val in list(HRuleStyle)
689
+ except AssertionError:
690
+ msg = f"Invalid value for {name}. Must be HRuleStyle."
691
+ raise ValueError(msg)
692
+
693
+ def _validate_vrules(self, name, val):
694
+ try:
695
+ assert val in list(VRuleStyle)
696
+ except AssertionError:
697
+ msg = f"Invalid value for {name}. Must be VRuleStyle."
698
+ raise ValueError(msg)
699
+
700
+ def _validate_field_name(self, name, val):
701
+ try:
702
+ assert (val in self._field_names) or (val is None)
703
+ except AssertionError:
704
+ msg = f"Invalid field name: {val}"
705
+ raise ValueError(msg)
706
+
707
+ def _validate_all_field_names(self, name, val):
708
+ try:
709
+ for x in val:
710
+ self._validate_field_name(name, x)
711
+ except AssertionError:
712
+ msg = "Fields must be a sequence of field names"
713
+ raise ValueError(msg)
714
+
715
+ def _validate_single_char(self, name, val):
716
+ try:
717
+ assert _str_block_width(val) == 1
718
+ except AssertionError:
719
+ msg = f"Invalid value for {name}. Must be a string of length 1."
720
+ raise ValueError(msg)
721
+
722
+ def _validate_attributes(self, name, val):
723
+ try:
724
+ assert isinstance(val, dict)
725
+ except AssertionError:
726
+ msg = "Attributes must be a dictionary of name/value pairs"
727
+ raise TypeError(msg)
728
+
729
+ ##############################
730
+ # ATTRIBUTE MANAGEMENT #
731
+ ##############################
732
+ @property
733
+ def rows(self) -> list[RowType]:
734
+ return self._rows[:]
735
+
736
+ @property
737
+ def dividers(self) -> list[bool]:
738
+ return self._dividers[:]
739
+
740
+ @property
741
+ def xhtml(self) -> bool:
742
+ """Print <br/> tags if True, <br> tags if False"""
743
+ return self._xhtml
744
+
745
+ @xhtml.setter
746
+ def xhtml(self, val: bool) -> None:
747
+ self._validate_option("xhtml", val)
748
+ self._xhtml = val
749
+
750
+ @property
751
+ def none_format(self):
752
+ return self._none_format
753
+
754
+ @none_format.setter
755
+ def none_format(self, val):
756
+ if not self._field_names:
757
+ self._none_format = {}
758
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
759
+ for field in self._field_names:
760
+ self._none_format[field] = None
761
+ else:
762
+ self._validate_none_format(val)
763
+ for field in self._field_names:
764
+ self._none_format[field] = val
765
+
766
+ @property
767
+ def field_names(self):
768
+ """List or tuple of field names
769
+
770
+ When setting field_names, if there are already field names the new list
771
+ of field names must be the same length. Columns are renamed and row data
772
+ remains unchanged."""
773
+ return self._field_names
774
+
775
+ @field_names.setter
776
+ def field_names(self, val) -> None:
777
+ val = [str(x) for x in val]
778
+ self._validate_option("field_names", val)
779
+ old_names = None
780
+ if self._field_names:
781
+ old_names = self._field_names[:]
782
+ self._field_names = val
783
+
784
+ self._column_specific_args()
785
+
786
+ if self._align and old_names:
787
+ for old_name, new_name in zip(old_names, val):
788
+ self._align[new_name] = self._align[old_name]
789
+ for old_name in old_names:
790
+ if old_name not in self._align:
791
+ self._align.pop(old_name)
792
+ elif self._align:
793
+ for field_name in self._field_names:
794
+ self._align[field_name] = self._align[BASE_ALIGN_VALUE]
795
+ else:
796
+ self.align = "c"
797
+ if self._valign and old_names:
798
+ for old_name, new_name in zip(old_names, val):
799
+ self._valign[new_name] = self._valign[old_name]
800
+ for old_name in old_names:
801
+ if old_name not in self._valign:
802
+ self._valign.pop(old_name)
803
+ else:
804
+ self.valign = "t"
805
+
806
+ @property
807
+ def align(self):
808
+ """Controls alignment of fields
809
+ Arguments:
810
+
811
+ align - alignment, one of "l", "c", or "r" """
812
+ return self._align
813
+
814
+ @align.setter
815
+ def align(self, val) -> None:
816
+ if val is None or (isinstance(val, dict) and len(val) == 0):
817
+ if not self._field_names:
818
+ self._align = {BASE_ALIGN_VALUE: "c"}
819
+ else:
820
+ for field in self._field_names:
821
+ self._align[field] = "c"
822
+ else:
823
+ self._validate_align(val)
824
+ if not self._field_names:
825
+ self._align = {BASE_ALIGN_VALUE: val}
826
+ else:
827
+ for field in self._field_names:
828
+ self._align[field] = val
829
+
830
+ @property
831
+ def valign(self):
832
+ """Controls vertical alignment of fields
833
+ Arguments:
834
+
835
+ valign - vertical alignment, one of "t", "m", or "b" """
836
+ return self._valign
837
+
838
+ @valign.setter
839
+ def valign(self, val) -> None:
840
+ if not self._field_names:
841
+ self._valign = {}
842
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
843
+ for field in self._field_names:
844
+ self._valign[field] = "t"
845
+ else:
846
+ self._validate_valign(val)
847
+ for field in self._field_names:
848
+ self._valign[field] = val
849
+
850
+ @property
851
+ def max_width(self):
852
+ """Controls maximum width of fields
853
+ Arguments:
854
+
855
+ max_width - maximum width integer"""
856
+ return self._max_width
857
+
858
+ @max_width.setter
859
+ def max_width(self, val) -> None:
860
+ if val is None or (isinstance(val, dict) and len(val) == 0):
861
+ self._max_width = {}
862
+ else:
863
+ self._validate_option("max_width", val)
864
+ for field in self._field_names:
865
+ self._max_width[field] = val
866
+
867
+ @property
868
+ def min_width(self):
869
+ """Controls minimum width of fields
870
+ Arguments:
871
+
872
+ min_width - minimum width integer"""
873
+ return self._min_width
874
+
875
+ @min_width.setter
876
+ def min_width(self, val) -> None:
877
+ if val is None or (isinstance(val, dict) and len(val) == 0):
878
+ self._min_width = {}
879
+ else:
880
+ self._validate_option("min_width", val)
881
+ for field in self._field_names:
882
+ self._min_width[field] = val
883
+
884
+ @property
885
+ def min_table_width(self) -> int | None:
886
+ return self._min_table_width
887
+
888
+ @min_table_width.setter
889
+ def min_table_width(self, val: int) -> None:
890
+ self._validate_option("min_table_width", val)
891
+ self._min_table_width = val
892
+
893
+ @property
894
+ def max_table_width(self) -> int | None:
895
+ return self._max_table_width
896
+
897
+ @max_table_width.setter
898
+ def max_table_width(self, val: int) -> None:
899
+ self._validate_option("max_table_width", val)
900
+ self._max_table_width = val
901
+
902
+ @property
903
+ def fields(self) -> Sequence[str | None] | None:
904
+ """List or tuple of field names to include in displays"""
905
+ return self._fields
906
+
907
+ @fields.setter
908
+ def fields(self, val: Sequence[str | None]) -> None:
909
+ self._validate_option("fields", val)
910
+ self._fields = val
911
+
912
+ @property
913
+ def title(self) -> str | None:
914
+ """Optional table title
915
+
916
+ Arguments:
917
+
918
+ title - table title"""
919
+ return self._title
920
+
921
+ @title.setter
922
+ def title(self, val: str) -> None:
923
+ self._title = str(val)
924
+
925
+ @property
926
+ def start(self) -> int:
927
+ """Start index of the range of rows to print
928
+
929
+ Arguments:
930
+
931
+ start - index of first data row to include in output"""
932
+ return self._start
933
+
934
+ @start.setter
935
+ def start(self, val: int) -> None:
936
+ self._validate_option("start", val)
937
+ self._start = val
938
+
939
+ @property
940
+ def end(self) -> int | None:
941
+ """End index of the range of rows to print
942
+
943
+ Arguments:
944
+
945
+ end - index of last data row to include in output PLUS ONE (list slice style)"""
946
+ return self._end
947
+
948
+ @end.setter
949
+ def end(self, val: int) -> None:
950
+ self._validate_option("end", val)
951
+ self._end = val
952
+
953
+ @property
954
+ def sortby(self) -> str | None:
955
+ """Name of field by which to sort rows
956
+
957
+ Arguments:
958
+
959
+ sortby - field name to sort by"""
960
+ return self._sortby
961
+
962
+ @sortby.setter
963
+ def sortby(self, val: str | None) -> None:
964
+ self._validate_option("sortby", val)
965
+ self._sortby = val
966
+
967
+ @property
968
+ def reversesort(self) -> bool:
969
+ """Controls direction of sorting (ascending vs descending)
970
+
971
+ Arguments:
972
+
973
+ reveresort - set to True to sort by descending order, or False to sort by
974
+ ascending order"""
975
+ return self._reversesort
976
+
977
+ @reversesort.setter
978
+ def reversesort(self, val: bool) -> None:
979
+ self._validate_option("reversesort", val)
980
+ self._reversesort = val
981
+
982
+ @property
983
+ def sort_key(self) -> Callable[[RowType], SupportsRichComparison]:
984
+ """Sorting key function, applied to data points before sorting
985
+
986
+ Arguments:
987
+
988
+ sort_key - a function which takes one argument and returns something to be
989
+ sorted"""
990
+ return self._sort_key
991
+
992
+ @sort_key.setter
993
+ def sort_key(self, val: Callable[[RowType], SupportsRichComparison]) -> None:
994
+ self._validate_option("sort_key", val)
995
+ self._sort_key = val
996
+
997
+ @property
998
+ def header(self) -> bool:
999
+ """Controls printing of table header with field names
1000
+
1001
+ Arguments:
1002
+
1003
+ header - print a header showing field names (True or False)"""
1004
+ return self._header
1005
+
1006
+ @header.setter
1007
+ def header(self, val: bool) -> None:
1008
+ self._validate_option("header", val)
1009
+ self._header = val
1010
+
1011
+ @property
1012
+ def header_style(self) -> HeaderStyleType:
1013
+ """Controls stylisation applied to field names in header
1014
+
1015
+ Arguments:
1016
+
1017
+ header_style - stylisation to apply to field names in header
1018
+ ("cap", "title", "upper", "lower" or None)"""
1019
+ return self._header_style
1020
+
1021
+ @header_style.setter
1022
+ def header_style(self, val: HeaderStyleType) -> None:
1023
+ self._validate_header_style(val)
1024
+ self._header_style = val
1025
+
1026
+ @property
1027
+ def border(self) -> bool:
1028
+ """Controls printing of border around table
1029
+
1030
+ Arguments:
1031
+
1032
+ border - print a border around the table (True or False)"""
1033
+ return self._border
1034
+
1035
+ @border.setter
1036
+ def border(self, val: bool) -> None:
1037
+ self._validate_option("border", val)
1038
+ self._border = val
1039
+
1040
+ @property
1041
+ def preserve_internal_border(self) -> bool:
1042
+ """Controls printing of border inside table
1043
+
1044
+ Arguments:
1045
+
1046
+ preserve_internal_border - print a border inside the table even if
1047
+ border is disabled (True or False)"""
1048
+ return self._preserve_internal_border
1049
+
1050
+ @preserve_internal_border.setter
1051
+ def preserve_internal_border(self, val: bool) -> None:
1052
+ self._validate_option("preserve_internal_border", val)
1053
+ self._preserve_internal_border = val
1054
+
1055
+ @property
1056
+ def hrules(self) -> HRuleStyle:
1057
+ """Controls printing of horizontal rules after rows
1058
+
1059
+ Arguments:
1060
+
1061
+ hrules - horizontal rules style. Allowed values: HRuleStyle"""
1062
+ return self._hrules
1063
+
1064
+ @hrules.setter
1065
+ def hrules(self, val: HRuleStyle) -> None:
1066
+ self._validate_option("hrules", val)
1067
+ self._hrules = val
1068
+
1069
+ @property
1070
+ def vrules(self) -> VRuleStyle:
1071
+ """Controls printing of vertical rules between columns
1072
+
1073
+ Arguments:
1074
+
1075
+ vrules - vertical rules style. Allowed values: VRuleStyle"""
1076
+ return self._vrules
1077
+
1078
+ @vrules.setter
1079
+ def vrules(self, val: VRuleStyle) -> None:
1080
+ self._validate_option("vrules", val)
1081
+ self._vrules = val
1082
+
1083
+ @property
1084
+ def int_format(self):
1085
+ """Controls formatting of integer data
1086
+ Arguments:
1087
+
1088
+ int_format - integer format string"""
1089
+ return self._int_format
1090
+
1091
+ @int_format.setter
1092
+ def int_format(self, val) -> None:
1093
+ if val is None or (isinstance(val, dict) and len(val) == 0):
1094
+ self._int_format = {}
1095
+ else:
1096
+ self._validate_option("int_format", val)
1097
+ for field in self._field_names:
1098
+ self._int_format[field] = val
1099
+
1100
+ @property
1101
+ def float_format(self):
1102
+ """Controls formatting of floating point data
1103
+ Arguments:
1104
+
1105
+ float_format - floating point format string"""
1106
+ return self._float_format
1107
+
1108
+ @float_format.setter
1109
+ def float_format(self, val) -> None:
1110
+ if val is None or (isinstance(val, dict) and len(val) == 0):
1111
+ self._float_format = {}
1112
+ else:
1113
+ self._validate_option("float_format", val)
1114
+ for field in self._field_names:
1115
+ self._float_format[field] = val
1116
+
1117
+ @property
1118
+ def custom_format(self):
1119
+ """Controls formatting of any column using callable
1120
+ Arguments:
1121
+
1122
+ custom_format - Dictionary of field_name and callable"""
1123
+ return self._custom_format
1124
+
1125
+ @custom_format.setter
1126
+ def custom_format(self, val):
1127
+ if val is None:
1128
+ self._custom_format = {}
1129
+ elif isinstance(val, dict):
1130
+ for k, v in val.items():
1131
+ self._validate_function(f"custom_value.{k}", v)
1132
+ self._custom_format = val
1133
+ elif hasattr(val, "__call__"):
1134
+ self._validate_function("custom_value", val)
1135
+ for field in self._field_names:
1136
+ self._custom_format[field] = val
1137
+ else:
1138
+ msg = "The custom_format property need to be a dictionary or callable"
1139
+ raise TypeError(msg)
1140
+
1141
+ @property
1142
+ def padding_width(self) -> int:
1143
+ """The number of empty spaces between a column's edge and its content
1144
+
1145
+ Arguments:
1146
+
1147
+ padding_width - number of spaces, must be a positive integer"""
1148
+ return self._padding_width
1149
+
1150
+ @padding_width.setter
1151
+ def padding_width(self, val: int) -> None:
1152
+ self._validate_option("padding_width", val)
1153
+ self._padding_width = val
1154
+
1155
+ @property
1156
+ def left_padding_width(self) -> int | None:
1157
+ """The number of empty spaces between a column's left edge and its content
1158
+
1159
+ Arguments:
1160
+
1161
+ left_padding - number of spaces, must be a positive integer"""
1162
+ return self._left_padding_width
1163
+
1164
+ @left_padding_width.setter
1165
+ def left_padding_width(self, val: int) -> None:
1166
+ self._validate_option("left_padding_width", val)
1167
+ self._left_padding_width = val
1168
+
1169
+ @property
1170
+ def right_padding_width(self) -> int | None:
1171
+ """The number of empty spaces between a column's right edge and its content
1172
+
1173
+ Arguments:
1174
+
1175
+ right_padding - number of spaces, must be a positive integer"""
1176
+ return self._right_padding_width
1177
+
1178
+ @right_padding_width.setter
1179
+ def right_padding_width(self, val: int) -> None:
1180
+ self._validate_option("right_padding_width", val)
1181
+ self._right_padding_width = val
1182
+
1183
+ @property
1184
+ def vertical_char(self) -> str:
1185
+ """The character used when printing table borders to draw vertical lines
1186
+
1187
+ Arguments:
1188
+
1189
+ vertical_char - single character string used to draw vertical lines"""
1190
+ return self._vertical_char
1191
+
1192
+ @vertical_char.setter
1193
+ def vertical_char(self, val: str) -> None:
1194
+ val = str(val)
1195
+ self._validate_option("vertical_char", val)
1196
+ self._vertical_char = val
1197
+
1198
+ @property
1199
+ def horizontal_char(self) -> str:
1200
+ """The character used when printing table borders to draw horizontal lines
1201
+
1202
+ Arguments:
1203
+
1204
+ horizontal_char - single character string used to draw horizontal lines"""
1205
+ return self._horizontal_char
1206
+
1207
+ @horizontal_char.setter
1208
+ def horizontal_char(self, val: str) -> None:
1209
+ val = str(val)
1210
+ self._validate_option("horizontal_char", val)
1211
+ self._horizontal_char = val
1212
+
1213
+ @property
1214
+ def horizontal_align_char(self) -> str:
1215
+ """The character used to indicate column alignment in horizontal lines
1216
+
1217
+ Arguments:
1218
+
1219
+ horizontal_align_char - single character string used to indicate alignment"""
1220
+ return self._bottom_left_junction_char or self.junction_char
1221
+
1222
+ @horizontal_align_char.setter
1223
+ def horizontal_align_char(self, val: str) -> None:
1224
+ val = str(val)
1225
+ self._validate_option("horizontal_align_char", val)
1226
+ self._horizontal_align_char = val
1227
+
1228
+ @property
1229
+ def junction_char(self) -> str:
1230
+ """The character used when printing table borders to draw line junctions
1231
+
1232
+ Arguments:
1233
+
1234
+ junction_char - single character string used to draw line junctions"""
1235
+ return self._junction_char
1236
+
1237
+ @junction_char.setter
1238
+ def junction_char(self, val: str) -> None:
1239
+ val = str(val)
1240
+ self._validate_option("junction_char", val)
1241
+ self._junction_char = val
1242
+
1243
+ @property
1244
+ def top_junction_char(self) -> str:
1245
+ """The character used when printing table borders to draw top line junctions
1246
+
1247
+ Arguments:
1248
+
1249
+ top_junction_char - single character string used to draw top line junctions"""
1250
+ return self._top_junction_char or self.junction_char
1251
+
1252
+ @top_junction_char.setter
1253
+ def top_junction_char(self, val: str) -> None:
1254
+ val = str(val)
1255
+ self._validate_option("top_junction_char", val)
1256
+ self._top_junction_char = val
1257
+
1258
+ @property
1259
+ def bottom_junction_char(self) -> str:
1260
+ """The character used when printing table borders to draw bottom line junctions
1261
+
1262
+ Arguments:
1263
+
1264
+ bottom_junction_char -
1265
+ single character string used to draw bottom line junctions"""
1266
+ return self._bottom_junction_char or self.junction_char
1267
+
1268
+ @bottom_junction_char.setter
1269
+ def bottom_junction_char(self, val: str) -> None:
1270
+ val = str(val)
1271
+ self._validate_option("bottom_junction_char", val)
1272
+ self._bottom_junction_char = val
1273
+
1274
+ @property
1275
+ def right_junction_char(self) -> str:
1276
+ """The character used when printing table borders to draw right line junctions
1277
+
1278
+ Arguments:
1279
+
1280
+ right_junction_char -
1281
+ single character string used to draw right line junctions"""
1282
+ return self._right_junction_char or self.junction_char
1283
+
1284
+ @right_junction_char.setter
1285
+ def right_junction_char(self, val: str) -> None:
1286
+ val = str(val)
1287
+ self._validate_option("right_junction_char", val)
1288
+ self._right_junction_char = val
1289
+
1290
+ @property
1291
+ def left_junction_char(self) -> str:
1292
+ """The character used when printing table borders to draw left line junctions
1293
+
1294
+ Arguments:
1295
+
1296
+ left_junction_char - single character string used to draw left line junctions"""
1297
+ return self._left_junction_char or self.junction_char
1298
+
1299
+ @left_junction_char.setter
1300
+ def left_junction_char(self, val: str) -> None:
1301
+ val = str(val)
1302
+ self._validate_option("left_junction_char", val)
1303
+ self._left_junction_char = val
1304
+
1305
+ @property
1306
+ def top_right_junction_char(self) -> str:
1307
+ """
1308
+ The character used when printing table borders to draw top-right line junctions
1309
+
1310
+ Arguments:
1311
+
1312
+ top_right_junction_char -
1313
+ single character string used to draw top-right line junctions"""
1314
+ return self._top_right_junction_char or self.junction_char
1315
+
1316
+ @top_right_junction_char.setter
1317
+ def top_right_junction_char(self, val: str) -> None:
1318
+ val = str(val)
1319
+ self._validate_option("top_right_junction_char", val)
1320
+ self._top_right_junction_char = val
1321
+
1322
+ @property
1323
+ def top_left_junction_char(self) -> str:
1324
+ """
1325
+ The character used when printing table borders to draw top-left line junctions
1326
+
1327
+ Arguments:
1328
+
1329
+ top_left_junction_char -
1330
+ single character string used to draw top-left line junctions"""
1331
+ return self._top_left_junction_char or self.junction_char
1332
+
1333
+ @top_left_junction_char.setter
1334
+ def top_left_junction_char(self, val: str) -> None:
1335
+ val = str(val)
1336
+ self._validate_option("top_left_junction_char", val)
1337
+ self._top_left_junction_char = val
1338
+
1339
+ @property
1340
+ def bottom_right_junction_char(self) -> str:
1341
+ """The character used when printing table borders
1342
+ to draw bottom-right line junctions
1343
+
1344
+ Arguments:
1345
+
1346
+ bottom_right_junction_char -
1347
+ single character string used to draw bottom-right line junctions"""
1348
+ return self._bottom_right_junction_char or self.junction_char
1349
+
1350
+ @bottom_right_junction_char.setter
1351
+ def bottom_right_junction_char(self, val: str) -> None:
1352
+ val = str(val)
1353
+ self._validate_option("bottom_right_junction_char", val)
1354
+ self._bottom_right_junction_char = val
1355
+
1356
+ @property
1357
+ def bottom_left_junction_char(self) -> str:
1358
+ """The character used when printing table borders
1359
+ to draw bottom-left line junctions
1360
+
1361
+ Arguments:
1362
+
1363
+ bottom_left_junction_char -
1364
+ single character string used to draw bottom-left line junctions"""
1365
+ return self._bottom_left_junction_char or self.junction_char
1366
+
1367
+ @bottom_left_junction_char.setter
1368
+ def bottom_left_junction_char(self, val: str) -> None:
1369
+ val = str(val)
1370
+ self._validate_option("bottom_left_junction_char", val)
1371
+ self._bottom_left_junction_char = val
1372
+
1373
+ @property
1374
+ def format(self) -> bool:
1375
+ """Controls whether or not HTML tables are formatted to match styling options
1376
+
1377
+ Arguments:
1378
+
1379
+ format - True or False"""
1380
+ return self._format
1381
+
1382
+ @format.setter
1383
+ def format(self, val: bool) -> None:
1384
+ self._validate_option("format", val)
1385
+ self._format = val
1386
+
1387
+ @property
1388
+ def print_empty(self) -> bool:
1389
+ """Controls whether or not empty tables produce a header and frame or just an
1390
+ empty string
1391
+
1392
+ Arguments:
1393
+
1394
+ print_empty - True or False"""
1395
+ return self._print_empty
1396
+
1397
+ @print_empty.setter
1398
+ def print_empty(self, val: bool) -> None:
1399
+ self._validate_option("print_empty", val)
1400
+ self._print_empty = val
1401
+
1402
+ @property
1403
+ def attributes(self) -> dict[str, str]:
1404
+ """A dictionary of HTML attribute name/value pairs to be included in the
1405
+ <table> tag when printing HTML
1406
+
1407
+ Arguments:
1408
+
1409
+ attributes - dictionary of attributes"""
1410
+ return self._attributes
1411
+
1412
+ @attributes.setter
1413
+ def attributes(self, val: dict[str, str]) -> None:
1414
+ self._validate_option("attributes", val)
1415
+ self._attributes = val
1416
+
1417
+ @property
1418
+ def oldsortslice(self) -> bool:
1419
+ """oldsortslice - Slice rows before sorting in the "old style" """
1420
+ return self._oldsortslice
1421
+
1422
+ @oldsortslice.setter
1423
+ def oldsortslice(self, val: bool) -> None:
1424
+ self._validate_option("oldsortslice", val)
1425
+ self._oldsortslice = val
1426
+
1427
+ @property
1428
+ def escape_header(self) -> bool:
1429
+ """Escapes the text within a header (True or False)"""
1430
+ return self._escape_header
1431
+
1432
+ @escape_header.setter
1433
+ def escape_header(self, val: bool) -> None:
1434
+ self._validate_option("escape_header", val)
1435
+ self._escape_header = val
1436
+
1437
+ @property
1438
+ def escape_data(self) -> bool:
1439
+ """Escapes the text within a data field (True or False)"""
1440
+ return self._escape_data
1441
+
1442
+ @escape_data.setter
1443
+ def escape_data(self, val: bool) -> None:
1444
+ self._validate_option("escape_data", val)
1445
+ self._escape_data = val
1446
+
1447
+ ##############################
1448
+ # OPTION MIXER #
1449
+ ##############################
1450
+
1451
+ def _get_options(self, kwargs: Mapping[str, Any]) -> OptionsType:
1452
+ options: dict[str, Any] = {}
1453
+ for option in self._options:
1454
+ if option in kwargs:
1455
+ self._validate_option(option, kwargs[option])
1456
+ options[option] = kwargs[option]
1457
+ else:
1458
+ options[option] = getattr(self, option)
1459
+ return cast(OptionsType, options)
1460
+
1461
+ ##############################
1462
+ # PRESET STYLE LOGIC #
1463
+ ##############################
1464
+
1465
+ def set_style(self, style: TableStyle) -> None:
1466
+ self._set_default_style()
1467
+ self._style = style
1468
+ if style == TableStyle.MSWORD_FRIENDLY:
1469
+ self._set_msword_style()
1470
+ elif style == TableStyle.PLAIN_COLUMNS:
1471
+ self._set_columns_style()
1472
+ elif style == TableStyle.MARKDOWN:
1473
+ self._set_markdown_style()
1474
+ elif style == TableStyle.ORGMODE:
1475
+ self._set_orgmode_style()
1476
+ elif style == TableStyle.DOUBLE_BORDER:
1477
+ self._set_double_border_style()
1478
+ elif style == TableStyle.SINGLE_BORDER:
1479
+ self._set_single_border_style()
1480
+ elif style == TableStyle.RANDOM:
1481
+ self._set_random_style()
1482
+ elif style != TableStyle.DEFAULT:
1483
+ msg = "Invalid pre-set style"
1484
+ raise ValueError(msg)
1485
+
1486
+ def _set_orgmode_style(self) -> None:
1487
+ self.orgmode = True
1488
+
1489
+ def _set_markdown_style(self) -> None:
1490
+ self.header = True
1491
+ self.border = True
1492
+ self._hrules = HRuleStyle.HEADER
1493
+ self.padding_width = 1
1494
+ self.left_padding_width = 1
1495
+ self.right_padding_width = 1
1496
+ self.vertical_char = "|"
1497
+ self.junction_char = "|"
1498
+ self._horizontal_align_char = ":"
1499
+
1500
+ def _set_default_style(self) -> None:
1501
+ self.header = True
1502
+ self.border = True
1503
+ self._hrules = HRuleStyle.FRAME
1504
+ self._vrules = VRuleStyle.ALL
1505
+ self.padding_width = 1
1506
+ self.left_padding_width = 1
1507
+ self.right_padding_width = 1
1508
+ self.vertical_char = "|"
1509
+ self.horizontal_char = "-"
1510
+ self._horizontal_align_char = None
1511
+ self.junction_char = "+"
1512
+ self._top_junction_char = None
1513
+ self._bottom_junction_char = None
1514
+ self._right_junction_char = None
1515
+ self._left_junction_char = None
1516
+ self._top_right_junction_char = None
1517
+ self._top_left_junction_char = None
1518
+ self._bottom_right_junction_char = None
1519
+ self._bottom_left_junction_char = None
1520
+
1521
+ def _set_msword_style(self) -> None:
1522
+ self.header = True
1523
+ self.border = True
1524
+ self._hrules = HRuleStyle.NONE
1525
+ self.padding_width = 1
1526
+ self.left_padding_width = 1
1527
+ self.right_padding_width = 1
1528
+ self.vertical_char = "|"
1529
+
1530
+ def _set_columns_style(self) -> None:
1531
+ self.header = True
1532
+ self.border = False
1533
+ self.padding_width = 1
1534
+ self.left_padding_width = 0
1535
+ self.right_padding_width = 8
1536
+
1537
+ def _set_double_border_style(self) -> None:
1538
+ self.horizontal_char = "═"
1539
+ self.vertical_char = "║"
1540
+ self.junction_char = "╬"
1541
+ self.top_junction_char = "╦"
1542
+ self.bottom_junction_char = "╩"
1543
+ self.right_junction_char = "╣"
1544
+ self.left_junction_char = "╠"
1545
+ self.top_right_junction_char = "╗"
1546
+ self.top_left_junction_char = "╔"
1547
+ self.bottom_right_junction_char = "╝"
1548
+ self.bottom_left_junction_char = "╚"
1549
+
1550
+ def _set_single_border_style(self) -> None:
1551
+ self.horizontal_char = "─"
1552
+ self.vertical_char = "│"
1553
+ self.junction_char = "┼"
1554
+ self.top_junction_char = "┬"
1555
+ self.bottom_junction_char = "┴"
1556
+ self.right_junction_char = "┤"
1557
+ self.left_junction_char = "├"
1558
+ self.top_right_junction_char = "┐"
1559
+ self.top_left_junction_char = "┌"
1560
+ self.bottom_right_junction_char = "┘"
1561
+ self.bottom_left_junction_char = "└"
1562
+
1563
+ def _set_random_style(self) -> None:
1564
+ # Just for fun!
1565
+ import random
1566
+
1567
+ self.header = random.choice((True, False))
1568
+ self.border = random.choice((True, False))
1569
+ self._hrules = random.choice(list(HRuleStyle))
1570
+ self._vrules = random.choice(list(VRuleStyle))
1571
+ self.left_padding_width = random.randint(0, 5)
1572
+ self.right_padding_width = random.randint(0, 5)
1573
+ self.vertical_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
1574
+ self.horizontal_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
1575
+ self.junction_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
1576
+ self.preserve_internal_border = random.choice((True, False))
1577
+
1578
+ ##############################
1579
+ # DATA INPUT METHODS #
1580
+ ##############################
1581
+
1582
+ def add_rows(self, rows: Iterable[RowType]) -> None:
1583
+ """Add rows to the table
1584
+
1585
+ Arguments:
1586
+
1587
+ rows - rows of data, should be an iterable of lists, each list with as many
1588
+ elements as the table has fields"""
1589
+ for row in rows:
1590
+ self.add_row(row)
1591
+
1592
+ def add_row(self, row: RowType, *, divider: bool = False) -> None:
1593
+ """Add a row to the table
1594
+
1595
+ Arguments:
1596
+
1597
+ row - row of data, should be a list with as many elements as the table
1598
+ has fields"""
1599
+
1600
+ if self._field_names and len(row) != len(self._field_names):
1601
+ msg = (
1602
+ "Row has incorrect number of values, "
1603
+ f"(actual) {len(row)}!={len(self._field_names)} (expected)"
1604
+ )
1605
+ raise ValueError(msg)
1606
+ if not self._field_names:
1607
+ self.field_names = [f"Field {n + 1}" for n in range(0, len(row))]
1608
+ self._rows.append(list(row))
1609
+ self._dividers.append(divider)
1610
+
1611
+ def del_row(self, row_index: int) -> None:
1612
+ """Delete a row from the table
1613
+
1614
+ Arguments:
1615
+
1616
+ row_index - The index of the row you want to delete. Indexing starts at 0."""
1617
+
1618
+ if row_index > len(self._rows) - 1:
1619
+ msg = (
1620
+ f"Can't delete row at index {row_index}, "
1621
+ f"table only has {len(self._rows)} rows"
1622
+ )
1623
+ raise IndexError(msg)
1624
+ del self._rows[row_index]
1625
+ del self._dividers[row_index]
1626
+
1627
+ def add_divider(self) -> None:
1628
+ """Add a divider to the table"""
1629
+ if len(self._dividers) >= 1:
1630
+ self._dividers[-1] = True
1631
+
1632
+ def add_column(
1633
+ self,
1634
+ fieldname: str,
1635
+ column: Sequence[Any],
1636
+ align: AlignType = "c",
1637
+ valign: VAlignType = "t",
1638
+ ) -> None:
1639
+ """Add a column to the table.
1640
+
1641
+ Arguments:
1642
+
1643
+ fieldname - name of the field to contain the new column of data
1644
+ column - column of data, should be a list with as many elements as the
1645
+ table has rows
1646
+ align - desired alignment for this column - "l" for left, "c" for centre and
1647
+ "r" for right
1648
+ valign - desired vertical alignment for new columns - "t" for top,
1649
+ "m" for middle and "b" for bottom"""
1650
+
1651
+ if len(self._rows) in (0, len(column)):
1652
+ self._validate_align(align)
1653
+ self._validate_valign(valign)
1654
+ self._field_names.append(fieldname)
1655
+ self._align[fieldname] = align
1656
+ self._valign[fieldname] = valign
1657
+ for i in range(0, len(column)):
1658
+ if len(self._rows) < i + 1:
1659
+ self._rows.append([])
1660
+ self._dividers.append(False)
1661
+ self._rows[i].append(column[i])
1662
+ else:
1663
+ msg = (
1664
+ f"Column length {len(column)} does not match number of rows "
1665
+ f"{len(self._rows)}"
1666
+ )
1667
+ raise ValueError(msg)
1668
+
1669
+ def add_autoindex(self, fieldname: str = "Index") -> None:
1670
+ """Add an auto-incrementing index column to the table.
1671
+ Arguments:
1672
+ fieldname - name of the field to contain the new column of data"""
1673
+ self._field_names.insert(0, fieldname)
1674
+ self._align[fieldname] = self._kwargs["align"] or "c"
1675
+ self._valign[fieldname] = self._kwargs["valign"] or "t"
1676
+ for i, row in enumerate(self._rows):
1677
+ row.insert(0, i + 1)
1678
+
1679
+ def del_column(self, fieldname: str) -> None:
1680
+ """Delete a column from the table
1681
+
1682
+ Arguments:
1683
+
1684
+ fieldname - The field name of the column you want to delete."""
1685
+
1686
+ if fieldname not in self._field_names:
1687
+ msg = (
1688
+ "Can't delete column {!r} which is not a field name of this table."
1689
+ " Field names are: {}".format(
1690
+ fieldname, ", ".join(map(repr, self._field_names))
1691
+ )
1692
+ )
1693
+ raise ValueError(msg)
1694
+
1695
+ col_index = self._field_names.index(fieldname)
1696
+ del self._field_names[col_index]
1697
+ for row in self._rows:
1698
+ del row[col_index]
1699
+
1700
+ def clear_rows(self) -> None:
1701
+ """Delete all rows from the table but keep the current field names"""
1702
+
1703
+ self._rows = []
1704
+ self._dividers = []
1705
+
1706
+ def clear(self) -> None:
1707
+ """Delete all rows and field names from the table, maintaining nothing but
1708
+ styling options"""
1709
+
1710
+ self._rows = []
1711
+ self._dividers = []
1712
+ self._field_names = []
1713
+ self._widths = []
1714
+
1715
+ ##############################
1716
+ # MISC PUBLIC METHODS #
1717
+ ##############################
1718
+
1719
+ def copy(self) -> Self:
1720
+ import copy
1721
+
1722
+ return copy.deepcopy(self)
1723
+
1724
+ def get_formatted_string(self, out_format: str = "text", **kwargs) -> str:
1725
+ """Return string representation of specified format of table in current state.
1726
+
1727
+ Arguments:
1728
+ out_format - resulting table format
1729
+ kwargs - passed through to function that performs formatting
1730
+ """
1731
+ if out_format == "text":
1732
+ return self.get_string(**kwargs)
1733
+ if out_format == "html":
1734
+ return self.get_html_string(**kwargs)
1735
+ if out_format == "json":
1736
+ return self.get_json_string(**kwargs)
1737
+ if out_format == "csv":
1738
+ return self.get_csv_string(**kwargs)
1739
+ if out_format == "latex":
1740
+ return self.get_latex_string(**kwargs)
1741
+
1742
+ msg = (
1743
+ f"Invalid format {out_format}. "
1744
+ "Must be one of: text, html, json, csv, or latex"
1745
+ )
1746
+ raise ValueError(msg)
1747
+
1748
+ ##############################
1749
+ # MISC PRIVATE METHODS #
1750
+ ##############################
1751
+
1752
+ def _format_value(self, field: str, value: Any) -> str:
1753
+ if isinstance(value, int) and field in self._int_format:
1754
+ return (f"%{self._int_format[field]}d") % value
1755
+ elif isinstance(value, float) and field in self._float_format:
1756
+ return (f"%{self._float_format[field]}f") % value
1757
+
1758
+ formatter = self._custom_format.get(field, (lambda f, v: str(v)))
1759
+ return formatter(field, value)
1760
+
1761
+ def _compute_table_width(self, options) -> int:
1762
+ if options["vrules"] == VRuleStyle.FRAME:
1763
+ table_width = 2
1764
+ if options["vrules"] == VRuleStyle.ALL:
1765
+ table_width = 1
1766
+ else:
1767
+ table_width = 0
1768
+ per_col_padding = sum(self._get_padding_widths(options))
1769
+ for index, fieldname in enumerate(self.field_names):
1770
+ if not options["fields"] or (
1771
+ options["fields"] and fieldname in options["fields"]
1772
+ ):
1773
+ table_width += self._widths[index] + per_col_padding + 1
1774
+ return table_width
1775
+
1776
+ def _compute_widths(self, rows: list[list[str]], options: OptionsType) -> None:
1777
+ if options["header"]:
1778
+ widths = [_get_size(field)[0] for field in self._field_names]
1779
+ else:
1780
+ widths = len(self.field_names) * [0]
1781
+
1782
+ for row in rows:
1783
+ for index, value in enumerate(row):
1784
+ fieldname = self.field_names[index]
1785
+ if (
1786
+ value == "None"
1787
+ and (none_val := self.none_format.get(fieldname)) is not None
1788
+ ):
1789
+ value = none_val
1790
+ if fieldname in self.max_width:
1791
+ widths[index] = max(
1792
+ widths[index],
1793
+ min(_get_size(value)[0], self.max_width[fieldname]),
1794
+ )
1795
+ else:
1796
+ widths[index] = max(widths[index], _get_size(value)[0])
1797
+ if fieldname in self.min_width:
1798
+ widths[index] = max(widths[index], self.min_width[fieldname])
1799
+
1800
+ if self._style == TableStyle.MARKDOWN:
1801
+ # Markdown needs at least one hyphen in the divider
1802
+ if self._align[fieldname] in ("l", "r"):
1803
+ min_width = 1
1804
+ else: # "c"
1805
+ min_width = 3
1806
+ widths[index] = max(min_width, widths[index])
1807
+
1808
+ self._widths = widths
1809
+
1810
+ per_col_padding = sum(self._get_padding_widths(options))
1811
+ # Are we exceeding max_table_width?
1812
+ if self._max_table_width:
1813
+ table_width = self._compute_table_width(options)
1814
+ if table_width > self._max_table_width:
1815
+ # Shrink widths in proportion
1816
+ markup_chars = per_col_padding * len(widths) + len(widths) - 1
1817
+ scale = (self._max_table_width - markup_chars) / (
1818
+ table_width - markup_chars
1819
+ )
1820
+ self._widths = [max(1, int(w * scale)) for w in widths]
1821
+
1822
+ # Are we under min_table_width or title width?
1823
+ if self._min_table_width or options["title"]:
1824
+ if options["title"]:
1825
+ title_width = len(options["title"]) + per_col_padding
1826
+ if options["vrules"] in (VRuleStyle.FRAME, VRuleStyle.ALL):
1827
+ title_width += 2
1828
+ else:
1829
+ title_width = 0
1830
+ min_table_width = self.min_table_width or 0
1831
+ min_width = max(title_width, min_table_width)
1832
+ if options["border"]:
1833
+ borders = len(widths) + 1
1834
+ elif options["preserve_internal_border"]:
1835
+ borders = len(widths)
1836
+ else:
1837
+ borders = 0
1838
+
1839
+ # Subtract padding for each column and borders
1840
+ min_width -= sum([per_col_padding for _ in widths]) + borders
1841
+ # What is being scaled is content so we sum column widths
1842
+ content_width = sum(widths) or 1
1843
+
1844
+ if content_width < min_width:
1845
+ # Grow widths in proportion
1846
+ scale = 1.0 * min_width / content_width
1847
+ widths = [int(w * scale) for w in widths]
1848
+ if sum(widths) < min_width:
1849
+ widths[-1] += min_width - sum(widths)
1850
+ self._widths = widths
1851
+
1852
+ def _get_padding_widths(self, options: OptionsType) -> tuple[int, int]:
1853
+ if options["left_padding_width"] is not None:
1854
+ lpad = options["left_padding_width"]
1855
+ else:
1856
+ lpad = options["padding_width"]
1857
+ if options["right_padding_width"] is not None:
1858
+ rpad = options["right_padding_width"]
1859
+ else:
1860
+ rpad = options["padding_width"]
1861
+ return lpad, rpad
1862
+
1863
+ def _get_rows(self, options: OptionsType) -> list[RowType]:
1864
+ """Return only those data rows that should be printed, based on slicing and
1865
+ sorting.
1866
+
1867
+ Arguments:
1868
+
1869
+ options - dictionary of option settings."""
1870
+ import copy
1871
+
1872
+ if options["oldsortslice"]:
1873
+ rows = copy.deepcopy(self._rows[options["start"]: options["end"]])
1874
+ else:
1875
+ rows = copy.deepcopy(self._rows)
1876
+
1877
+ # Sort
1878
+ if options["sortby"]:
1879
+ sortindex = self._field_names.index(options["sortby"])
1880
+ # Decorate
1881
+ rows = [[row[sortindex]] + row for row in rows]
1882
+ # Sort
1883
+ rows.sort(reverse=options["reversesort"], key=options["sort_key"])
1884
+ # Undecorate
1885
+ rows = [row[1:] for row in rows]
1886
+
1887
+ # Slice if necessary
1888
+ if not options["oldsortslice"]:
1889
+ rows = rows[options["start"]: options["end"]]
1890
+
1891
+ return rows
1892
+
1893
+ def _get_dividers(self, options: OptionsType) -> list[bool]:
1894
+ """Return only those dividers that should be printed, based on slicing.
1895
+
1896
+ Arguments:
1897
+
1898
+ options - dictionary of option settings."""
1899
+ import copy
1900
+
1901
+ if options["oldsortslice"]:
1902
+ dividers = copy.deepcopy(self._dividers[options["start"]: options["end"]])
1903
+ else:
1904
+ dividers = copy.deepcopy(self._dividers)
1905
+
1906
+ if options["sortby"]:
1907
+ dividers = [False for divider in dividers]
1908
+
1909
+ return dividers
1910
+
1911
+ def _format_row(self, row: RowType) -> list[str]:
1912
+ return [
1913
+ self._format_value(field, value)
1914
+ for (field, value) in zip(self._field_names, row)
1915
+ ]
1916
+
1917
+ def _format_rows(self, rows: list[RowType]) -> list[list[str]]:
1918
+ return [self._format_row(row) for row in rows]
1919
+
1920
+ ##############################
1921
+ # PLAIN TEXT STRING METHODS #
1922
+ ##############################
1923
+
1924
+ def get_string(self, **kwargs) -> str:
1925
+ """Return string representation of table in current state.
1926
+
1927
+ Arguments:
1928
+
1929
+ title - optional table title
1930
+ start - index of first data row to include in output
1931
+ end - index of last data row to include in output PLUS ONE (list slice style)
1932
+ fields - names of fields (columns) to include
1933
+ header - print a header showing field names (True or False)
1934
+ border - print a border around the table (True or False)
1935
+ preserve_internal_border - print a border inside the table even if
1936
+ border is disabled (True or False)
1937
+ hrules - controls printing of horizontal rules after rows.
1938
+ Allowed values: HRuleStyle
1939
+ vrules - controls printing of vertical rules between columns.
1940
+ Allowed values: VRuleStyle
1941
+ int_format - controls formatting of integer data
1942
+ float_format - controls formatting of floating point data
1943
+ custom_format - controls formatting of any column using callable
1944
+ padding_width - number of spaces on either side of column data (only used if
1945
+ left and right paddings are None)
1946
+ left_padding_width - number of spaces on left hand side of column data
1947
+ right_padding_width - number of spaces on right hand side of column data
1948
+ vertical_char - single character string used to draw vertical lines
1949
+ horizontal_char - single character string used to draw horizontal lines
1950
+ horizontal_align_char - single character string used to indicate alignment
1951
+ junction_char - single character string used to draw line junctions
1952
+ junction_char - single character string used to draw line junctions
1953
+ top_junction_char - single character string used to draw top line junctions
1954
+ bottom_junction_char -
1955
+ single character string used to draw bottom line junctions
1956
+ right_junction_char - single character string used to draw right line junctions
1957
+ left_junction_char - single character string used to draw left line junctions
1958
+ top_right_junction_char -
1959
+ single character string used to draw top-right line junctions
1960
+ top_left_junction_char -
1961
+ single character string used to draw top-left line junctions
1962
+ bottom_right_junction_char -
1963
+ single character string used to draw bottom-right line junctions
1964
+ bottom_left_junction_char -
1965
+ single character string used to draw bottom-left line junctions
1966
+ sortby - name of field to sort rows by
1967
+ sort_key - sorting key function, applied to data points before sorting
1968
+ reversesort - True or False to sort in descending or ascending order
1969
+ print empty - if True, stringify just the header for an empty table,
1970
+ if False return an empty string"""
1971
+
1972
+ options = self._get_options(kwargs)
1973
+
1974
+ lines: list[str] = []
1975
+
1976
+ # Don't think too hard about an empty table
1977
+ # Is this the desired behaviour? Maybe we should still print the header?
1978
+ if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
1979
+ return ""
1980
+
1981
+ # Get the rows we need to print, taking into account slicing, sorting, etc.
1982
+ rows = self._get_rows(options)
1983
+ dividers = self._get_dividers(options)
1984
+
1985
+ # Turn all data in all rows into Unicode, formatted as desired
1986
+ formatted_rows = self._format_rows(rows)
1987
+
1988
+ # Compute column widths
1989
+ self._compute_widths(formatted_rows, options)
1990
+ self._hrule = self._stringify_hrule(options)
1991
+
1992
+ # Add title
1993
+ title = options["title"] or self._title
1994
+ if title:
1995
+ lines.append(self._stringify_title(title, options))
1996
+
1997
+ # Add header or top of border
1998
+ if options["header"]:
1999
+ lines.append(self._stringify_header(options))
2000
+ elif options["border"] and options["hrules"] in (
2001
+ HRuleStyle.ALL,
2002
+ HRuleStyle.FRAME,
2003
+ ):
2004
+ lines.append(self._stringify_hrule(options, where="top_"))
2005
+ if title and options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2006
+ left_j_len = len(self.left_junction_char)
2007
+ right_j_len = len(self.right_junction_char)
2008
+ lines[-1] = (
2009
+ self.left_junction_char
2010
+ + lines[-1][left_j_len:-right_j_len]
2011
+ + self.right_junction_char
2012
+ )
2013
+
2014
+ # Add rows
2015
+ for row, divider in zip(formatted_rows[:-1], dividers[:-1]):
2016
+ lines.append(self._stringify_row(row, options, self._hrule))
2017
+ if divider:
2018
+ lines.append(self._stringify_hrule(options))
2019
+ if formatted_rows:
2020
+ lines.append(
2021
+ self._stringify_row(
2022
+ formatted_rows[-1],
2023
+ options,
2024
+ self._stringify_hrule(options, where="bottom_"),
2025
+ )
2026
+ )
2027
+
2028
+ # Add bottom of border
2029
+ if options["border"] and options["hrules"] == HRuleStyle.FRAME:
2030
+ lines.append(self._stringify_hrule(options, where="bottom_"))
2031
+
2032
+ if "orgmode" in self.__dict__ and self.orgmode:
2033
+ left_j_len = len(self.left_junction_char)
2034
+ right_j_len = len(self.right_junction_char)
2035
+ lines = [
2036
+ "|" + new_line[left_j_len:-right_j_len] + "|"
2037
+ for old_line in lines
2038
+ for new_line in old_line.split("\n")
2039
+ ]
2040
+
2041
+ return "\n".join(lines)
2042
+
2043
+ def _stringify_hrule(
2044
+ self, options: OptionsType, where: Literal["top_", "bottom_", ""] = ""
2045
+ ) -> str:
2046
+ if not options["border"] and not options["preserve_internal_border"]:
2047
+ return ""
2048
+ lpad, rpad = self._get_padding_widths(options)
2049
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2050
+ bits = [options[where + "left_junction_char"]] # type: ignore[literal-required]
2051
+ else:
2052
+ bits = [options["horizontal_char"]]
2053
+ # For tables with no data or fieldnames
2054
+ if not self._field_names:
2055
+ bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
2056
+ return "".join(bits)
2057
+ for field, width in zip(self._field_names, self._widths):
2058
+ if options["fields"] and field not in options["fields"]:
2059
+ continue
2060
+
2061
+ line = (width + lpad + rpad) * options["horizontal_char"]
2062
+
2063
+ # If necessary, add column alignment characters (e.g. ":" for Markdown)
2064
+ if self._horizontal_align_char:
2065
+ if self._align[field] in ("l", "c"):
2066
+ line = " " + self._horizontal_align_char + line[2:]
2067
+ if self._align[field] in ("c", "r"):
2068
+ line = line[:-2] + self._horizontal_align_char + " "
2069
+
2070
+ bits.append(line)
2071
+ if options["vrules"] == VRuleStyle.ALL:
2072
+ bits.append(options[where + "junction_char"]) # type: ignore[literal-required]
2073
+ else:
2074
+ bits.append(options["horizontal_char"])
2075
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2076
+ bits.pop()
2077
+ bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
2078
+
2079
+ if options["preserve_internal_border"] and not options["border"]:
2080
+ bits = bits[1:-1]
2081
+
2082
+ return "".join(bits)
2083
+
2084
+ def _stringify_title(self, title: str, options: OptionsType) -> str:
2085
+ lines: list[str] = []
2086
+ lpad, rpad = self._get_padding_widths(options)
2087
+ if options["border"]:
2088
+ if options["vrules"] == VRuleStyle.ALL:
2089
+ options["vrules"] = VRuleStyle.FRAME
2090
+ lines.append(self._stringify_hrule(options, "top_"))
2091
+ options["vrules"] = VRuleStyle.ALL
2092
+ elif options["vrules"] == VRuleStyle.FRAME:
2093
+ lines.append(self._stringify_hrule(options, "top_"))
2094
+ bits: list[str] = []
2095
+ endpoint = (
2096
+ options["vertical_char"]
2097
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME)
2098
+ and options["border"]
2099
+ else " "
2100
+ )
2101
+ bits.append(endpoint)
2102
+ title = " " * lpad + title + " " * rpad
2103
+ lpad, rpad = self._get_padding_widths(options)
2104
+ sum_widths = sum([n + lpad + rpad + 1 for n in self._widths])
2105
+
2106
+ bits.append(self._justify(title, sum_widths - 1, "c"))
2107
+ bits.append(endpoint)
2108
+ lines.append("".join(bits))
2109
+ return "\n".join(lines)
2110
+
2111
+ def _stringify_header(self, options: OptionsType) -> str:
2112
+ bits: list[str] = []
2113
+ lpad, rpad = self._get_padding_widths(options)
2114
+ if options["border"]:
2115
+ if options["hrules"] in (HRuleStyle.ALL, HRuleStyle.FRAME):
2116
+ bits.append(self._stringify_hrule(options, "top_"))
2117
+ if options["title"] and options["vrules"] in (
2118
+ VRuleStyle.ALL,
2119
+ VRuleStyle.FRAME,
2120
+ ):
2121
+ left_j_len = len(self.left_junction_char)
2122
+ right_j_len = len(self.right_junction_char)
2123
+ bits[-1] = (
2124
+ self.left_junction_char
2125
+ + bits[-1][left_j_len:-right_j_len]
2126
+ + self.right_junction_char
2127
+ )
2128
+ bits.append("\n")
2129
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2130
+ bits.append(options["vertical_char"])
2131
+ else:
2132
+ bits.append(" ")
2133
+ # For tables with no data or field names
2134
+ if not self._field_names:
2135
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2136
+ bits.append(options["vertical_char"])
2137
+ else:
2138
+ bits.append(" ")
2139
+ for field, width in zip(self._field_names, self._widths):
2140
+ if options["fields"] and field not in options["fields"]:
2141
+ continue
2142
+ if self._header_style == "cap":
2143
+ fieldname = field.capitalize()
2144
+ elif self._header_style == "title":
2145
+ fieldname = field.title()
2146
+ elif self._header_style == "upper":
2147
+ fieldname = field.upper()
2148
+ elif self._header_style == "lower":
2149
+ fieldname = field.lower()
2150
+ else:
2151
+ fieldname = field
2152
+ if _str_block_width(fieldname) > width:
2153
+ fieldname = fieldname[:width]
2154
+ bits.append(
2155
+ " " * lpad
2156
+ + self._justify(fieldname, width, self._align[field])
2157
+ + " " * rpad
2158
+ )
2159
+ if options["border"] or options["preserve_internal_border"]:
2160
+ if options["vrules"] == VRuleStyle.ALL:
2161
+ bits.append(options["vertical_char"])
2162
+ else:
2163
+ bits.append(" ")
2164
+
2165
+ # If only preserve_internal_border is true, then we just appended
2166
+ # a vertical character at the end when we wanted a space
2167
+ if not options["border"] and options["preserve_internal_border"]:
2168
+ bits.pop()
2169
+ bits.append(" ")
2170
+ # If vrules is FRAME, then we just appended a space at the end
2171
+ # of the last field, when we really want a vertical character
2172
+ if options["border"] and options["vrules"] == VRuleStyle.FRAME:
2173
+ bits.pop()
2174
+ bits.append(options["vertical_char"])
2175
+ if (options["border"] or options["preserve_internal_border"]) and options[
2176
+ "hrules"
2177
+ ] != HRuleStyle.NONE:
2178
+ bits.append("\n")
2179
+ bits.append(self._hrule)
2180
+ return "".join(bits)
2181
+
2182
+ def _stringify_row(self, row: list[str], options: OptionsType, hrule: str) -> str:
2183
+ import textwrap
2184
+
2185
+ for index, field, value, width in zip(
2186
+ range(0, len(row)), self._field_names, row, self._widths
2187
+ ):
2188
+ # Enforce max widths
2189
+ lines = value.split("\n")
2190
+ new_lines: list[str] = []
2191
+ for line in lines:
2192
+ if (
2193
+ line == "None"
2194
+ and (none_val := self.none_format.get(field)) is not None
2195
+ ):
2196
+ line = none_val
2197
+ if _str_block_width(line) > width:
2198
+ line = textwrap.fill(line, width)
2199
+ new_lines.append(line)
2200
+ lines = new_lines
2201
+ value = "\n".join(lines)
2202
+ row[index] = value
2203
+
2204
+ row_height = 0
2205
+ for c in row:
2206
+ h = _get_size(c)[1]
2207
+ if h > row_height:
2208
+ row_height = h
2209
+
2210
+ bits: list[list[str]] = []
2211
+ lpad, rpad = self._get_padding_widths(options)
2212
+ for y in range(0, row_height):
2213
+ bits.append([])
2214
+ if options["border"]:
2215
+ if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
2216
+ bits[y].append(self.vertical_char)
2217
+ else:
2218
+ bits[y].append(" ")
2219
+
2220
+ for field, value, width in zip(self._field_names, row, self._widths):
2221
+ valign = self._valign[field]
2222
+ lines = value.split("\n")
2223
+ d_height = row_height - len(lines)
2224
+ if d_height:
2225
+ if valign == "m":
2226
+ lines = (
2227
+ [""] * int(d_height / 2)
2228
+ + lines
2229
+ + [""] * (d_height - int(d_height / 2))
2230
+ )
2231
+ elif valign == "b":
2232
+ lines = [""] * d_height + lines
2233
+ else:
2234
+ lines = lines + [""] * d_height
2235
+
2236
+ for y, line in enumerate(lines):
2237
+ if options["fields"] and field not in options["fields"]:
2238
+ continue
2239
+
2240
+ bits[y].append(
2241
+ " " * lpad
2242
+ + self._justify(line, width, self._align[field])
2243
+ + " " * rpad
2244
+ )
2245
+ if options["border"] or options["preserve_internal_border"]:
2246
+ if options["vrules"] == VRuleStyle.ALL:
2247
+ bits[y].append(self.vertical_char)
2248
+ else:
2249
+ bits[y].append(" ")
2250
+
2251
+ # If only preserve_internal_border is true, then we just appended
2252
+ # a vertical character at the end when we wanted a space
2253
+ if not options["border"] and options["preserve_internal_border"]:
2254
+ bits[-1].pop()
2255
+ bits[-1].append(" ")
2256
+
2257
+ # If vrules is FRAME, then we just appended a space at the end
2258
+ # of the last field, when we really want a vertical character
2259
+ for y in range(0, row_height):
2260
+ if options["border"] and options["vrules"] == VRuleStyle.FRAME:
2261
+ bits[y].pop()
2262
+ bits[y].append(options["vertical_char"])
2263
+
2264
+ if options["border"] and options["hrules"] == HRuleStyle.ALL:
2265
+ bits[row_height - 1].append("\n")
2266
+ bits[row_height - 1].append(hrule)
2267
+
2268
+ bits_str = ["".join(bits_y) for bits_y in bits]
2269
+ return "\n".join(bits_str)
2270
+
2271
+ def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs) -> str:
2272
+ pages: list[str] = []
2273
+ kwargs["start"] = kwargs.get("start", 0)
2274
+ true_end = kwargs.get("end", self.rowcount)
2275
+ while True:
2276
+ kwargs["end"] = min(kwargs["start"] + page_length, true_end)
2277
+ pages.append(self.get_string(**kwargs))
2278
+ if kwargs["end"] == true_end:
2279
+ break
2280
+ kwargs["start"] += page_length
2281
+ return line_break.join(pages)
2282
+
2283
+ ##############################
2284
+ # CSV STRING METHODS #
2285
+ ##############################
2286
+ def get_csv_string(self, **kwargs) -> str:
2287
+ """Return string representation of CSV formatted table in the current state
2288
+
2289
+ Keyword arguments are first interpreted as table formatting options, and
2290
+ then any unused keyword arguments are passed to csv.writer(). For
2291
+ example, get_csv_string(header=False, delimiter='\t') would use
2292
+ header as a PrettyTable formatting option (skip the header row) and
2293
+ delimiter as a csv.writer keyword argument.
2294
+ """
2295
+ import csv
2296
+
2297
+ options = self._get_options(kwargs)
2298
+ csv_options = {
2299
+ key: value for key, value in kwargs.items() if key not in options
2300
+ }
2301
+ csv_buffer = io.StringIO()
2302
+ csv_writer = csv.writer(csv_buffer, **csv_options)
2303
+
2304
+ if options.get("header"):
2305
+ if options["fields"]:
2306
+ csv_writer.writerow(
2307
+ [f for f in self._field_names if f in options["fields"]]
2308
+ )
2309
+ else:
2310
+ csv_writer.writerow(self._field_names)
2311
+
2312
+ rows = self._get_rows(options)
2313
+ if options["fields"]:
2314
+ rows = [
2315
+ [d for f, d in zip(self._field_names, row) if f in options["fields"]]
2316
+ for row in rows
2317
+ ]
2318
+ for row in rows:
2319
+ csv_writer.writerow(row)
2320
+
2321
+ return csv_buffer.getvalue()
2322
+
2323
+ ##############################
2324
+ # JSON STRING METHODS #
2325
+ ##############################
2326
+ def get_json_string(self, **kwargs) -> str:
2327
+ """Return string representation of JSON formatted table in the current state
2328
+
2329
+ Keyword arguments are first interpreted as table formatting options, and
2330
+ then any unused keyword arguments are passed to json.dumps(). For
2331
+ example, get_json_string(header=False, indent=2) would use header as
2332
+ a PrettyTable formatting option (skip the header row) and indent as a
2333
+ json.dumps keyword argument.
2334
+ """
2335
+ import json
2336
+
2337
+ options = self._get_options(kwargs)
2338
+ json_options: dict[str, Any] = {
2339
+ "indent": 4,
2340
+ "separators": (",", ": "),
2341
+ "sort_keys": True,
2342
+ }
2343
+ json_options.update(
2344
+ {key: value for key, value in kwargs.items() if key not in options}
2345
+ )
2346
+ objects: list[list[str] | dict[str, Any]] = []
2347
+
2348
+ if options.get("header"):
2349
+ if options["fields"]:
2350
+ objects.append([f for f in self._field_names if f in options["fields"]])
2351
+ else:
2352
+ objects.append(self.field_names)
2353
+ rows = self._get_rows(options)
2354
+ if options["fields"]:
2355
+ for row in rows:
2356
+ objects.append(
2357
+ {
2358
+ f: d
2359
+ for f, d in zip(self._field_names, row)
2360
+ if f in options["fields"]
2361
+ }
2362
+ )
2363
+ else:
2364
+ for row in rows:
2365
+ objects.append(dict(zip(self._field_names, row)))
2366
+
2367
+ return json.dumps(objects, **json_options)
2368
+
2369
+ ##############################
2370
+ # HTML STRING METHODS #
2371
+ ##############################
2372
+
2373
+ def get_html_string(self, **kwargs) -> str:
2374
+ """Return string representation of HTML formatted version of table in current
2375
+ state.
2376
+
2377
+ Arguments:
2378
+
2379
+ title - optional table title
2380
+ start - index of first data row to include in output
2381
+ end - index of last data row to include in output PLUS ONE (list slice style)
2382
+ fields - names of fields (columns) to include
2383
+ header - print a header showing field names (True or False)
2384
+ escape_header - escapes the text within a header (True or False)
2385
+ border - print a border around the table (True or False)
2386
+ preserve_internal_border - print a border inside the table even if
2387
+ border is disabled (True or False)
2388
+ hrules - controls printing of horizontal rules after rows.
2389
+ Allowed values: HRuleStyle
2390
+ vrules - controls printing of vertical rules between columns.
2391
+ Allowed values: VRuleStyle
2392
+ int_format - controls formatting of integer data
2393
+ float_format - controls formatting of floating point data
2394
+ custom_format - controls formatting of any column using callable
2395
+ padding_width - number of spaces on either side of column data (only used if
2396
+ left and right paddings are None)
2397
+ left_padding_width - number of spaces on left hand side of column data
2398
+ right_padding_width - number of spaces on right hand side of column data
2399
+ sortby - name of field to sort rows by
2400
+ sort_key - sorting key function, applied to data points before sorting
2401
+ attributes - dictionary of name/value pairs to include as HTML attributes in the
2402
+ <table> tag
2403
+ format - Controls whether or not HTML tables are formatted to match
2404
+ styling options (True or False)
2405
+ escape_data - escapes the text within a data field (True or False)
2406
+ xhtml - print <br/> tags if True, <br> tags if False"""
2407
+
2408
+ options = self._get_options(kwargs)
2409
+
2410
+ if options["format"]:
2411
+ string = self._get_formatted_html_string(options)
2412
+ else:
2413
+ string = self._get_simple_html_string(options)
2414
+
2415
+ return string
2416
+
2417
+ def _get_simple_html_string(self, options: OptionsType) -> str:
2418
+ from html import escape
2419
+
2420
+ lines: list[str] = []
2421
+ if options["xhtml"]:
2422
+ linebreak = "<br/>"
2423
+ else:
2424
+ linebreak = "<br>"
2425
+
2426
+ open_tag = ["<table"]
2427
+ if options["attributes"]:
2428
+ for attr_name, attr_value in options["attributes"].items():
2429
+ open_tag.append(f' {escape(attr_name)}="{escape(attr_value)}"')
2430
+ open_tag.append(">")
2431
+ lines.append("".join(open_tag))
2432
+
2433
+ # Title
2434
+ title = options["title"] or self._title
2435
+ if title:
2436
+ lines.append(f" <caption>{escape(title)}</caption>")
2437
+
2438
+ # Headers
2439
+ if options["header"]:
2440
+ lines.append(" <thead>")
2441
+ lines.append(" <tr>")
2442
+ for field in self._field_names:
2443
+ if options["fields"] and field not in options["fields"]:
2444
+ continue
2445
+ if options["escape_header"]:
2446
+ field = escape(field)
2447
+
2448
+ lines.append(
2449
+ " <th>{}</th>".format(field.replace("\n", linebreak))
2450
+ )
2451
+
2452
+ lines.append(" </tr>")
2453
+ lines.append(" </thead>")
2454
+
2455
+ # Data
2456
+ lines.append(" <tbody>")
2457
+ rows = self._get_rows(options)
2458
+ formatted_rows = self._format_rows(rows)
2459
+ for row in formatted_rows:
2460
+ lines.append(" <tr>")
2461
+ for field, datum in zip(self._field_names, row):
2462
+ if options["fields"] and field not in options["fields"]:
2463
+ continue
2464
+ if options["escape_data"]:
2465
+ datum = escape(datum)
2466
+
2467
+ lines.append(
2468
+ " <td>{}</td>".format(datum.replace("\n", linebreak))
2469
+ )
2470
+ lines.append(" </tr>")
2471
+ lines.append(" </tbody>")
2472
+ lines.append("</table>")
2473
+
2474
+ return "\n".join(lines)
2475
+
2476
+ def _get_formatted_html_string(self, options: OptionsType) -> str:
2477
+ from html import escape
2478
+
2479
+ lines: list[str] = []
2480
+ lpad, rpad = self._get_padding_widths(options)
2481
+ if options["xhtml"]:
2482
+ linebreak = "<br/>"
2483
+ else:
2484
+ linebreak = "<br>"
2485
+
2486
+ open_tag = ["<table"]
2487
+ if options["border"]:
2488
+ if (
2489
+ options["hrules"] == HRuleStyle.ALL
2490
+ and options["vrules"] == VRuleStyle.ALL
2491
+ ):
2492
+ open_tag.append(' frame="box" rules="all"')
2493
+ elif (
2494
+ options["hrules"] == HRuleStyle.FRAME
2495
+ and options["vrules"] == VRuleStyle.FRAME
2496
+ ):
2497
+ open_tag.append(' frame="box"')
2498
+ elif (
2499
+ options["hrules"] == HRuleStyle.FRAME
2500
+ and options["vrules"] == VRuleStyle.ALL
2501
+ ):
2502
+ open_tag.append(' frame="box" rules="cols"')
2503
+ elif options["hrules"] == HRuleStyle.FRAME:
2504
+ open_tag.append(' frame="hsides"')
2505
+ elif options["hrules"] == HRuleStyle.ALL:
2506
+ open_tag.append(' frame="hsides" rules="rows"')
2507
+ elif options["vrules"] == VRuleStyle.FRAME:
2508
+ open_tag.append(' frame="vsides"')
2509
+ elif options["vrules"] == VRuleStyle.ALL:
2510
+ open_tag.append(' frame="vsides" rules="cols"')
2511
+ if not options["border"] and options["preserve_internal_border"]:
2512
+ open_tag.append(' rules="cols"')
2513
+ if options["attributes"]:
2514
+ for attr_name, attr_value in options["attributes"].items():
2515
+ open_tag.append(f' {escape(attr_name)}="{escape(attr_value)}"')
2516
+ open_tag.append(">")
2517
+ lines.append("".join(open_tag))
2518
+
2519
+ # Title
2520
+ title = options["title"] or self._title
2521
+ if title:
2522
+ lines.append(f" <caption>{escape(title)}</caption>")
2523
+
2524
+ # Headers
2525
+ if options["header"]:
2526
+ lines.append(" <thead>")
2527
+ lines.append(" <tr>")
2528
+ for field in self._field_names:
2529
+ if options["fields"] and field not in options["fields"]:
2530
+ continue
2531
+ if options["escape_header"]:
2532
+ field = escape(field)
2533
+
2534
+ content = field.replace("\n", linebreak)
2535
+ lines.append(
2536
+ f' <th style="'
2537
+ f"padding-left: {lpad}em; "
2538
+ f"padding-right: {rpad}em; "
2539
+ f'text-align: center">{content}</th>'
2540
+ )
2541
+ lines.append(" </tr>")
2542
+ lines.append(" </thead>")
2543
+
2544
+ # Data
2545
+ lines.append(" <tbody>")
2546
+ rows = self._get_rows(options)
2547
+ formatted_rows = self._format_rows(rows)
2548
+ aligns: list[str] = []
2549
+ valigns: list[str] = []
2550
+ for field in self._field_names:
2551
+ aligns.append(
2552
+ {"l": "left", "r": "right", "c": "center"}[self._align[field]]
2553
+ )
2554
+ valigns.append(
2555
+ {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
2556
+ )
2557
+ for row in formatted_rows:
2558
+ lines.append(" <tr>")
2559
+ for field, datum, align, valign in zip(
2560
+ self._field_names, row, aligns, valigns
2561
+ ):
2562
+ if options["fields"] and field not in options["fields"]:
2563
+ continue
2564
+ if options["escape_data"]:
2565
+ datum = escape(datum)
2566
+
2567
+ content = datum.replace("\n", linebreak)
2568
+ lines.append(
2569
+ f' <td style="'
2570
+ f"padding-left: {lpad}em; "
2571
+ f"padding-right: {rpad}em; "
2572
+ f"text-align: {align}; "
2573
+ f'vertical-align: {valign}">{content}</td>'
2574
+ )
2575
+ lines.append(" </tr>")
2576
+ lines.append(" </tbody>")
2577
+ lines.append("</table>")
2578
+
2579
+ return "\n".join(lines)
2580
+
2581
+ ##############################
2582
+ # LATEX STRING METHODS #
2583
+ ##############################
2584
+
2585
+ def get_latex_string(self, **kwargs) -> str:
2586
+ """Return string representation of LaTex formatted version of table in current
2587
+ state.
2588
+
2589
+ Arguments:
2590
+
2591
+ start - index of first data row to include in output
2592
+ end - index of last data row to include in output PLUS ONE (list slice style)
2593
+ fields - names of fields (columns) to include
2594
+ header - print a header showing field names (True or False)
2595
+ border - print a border around the table (True or False)
2596
+ preserve_internal_border - print a border inside the table even if
2597
+ border is disabled (True or False)
2598
+ hrules - controls printing of horizontal rules after rows.
2599
+ Allowed values: HRuleStyle
2600
+ vrules - controls printing of vertical rules between columns.
2601
+ Allowed values: VRuleStyle
2602
+ int_format - controls formatting of integer data
2603
+ float_format - controls formatting of floating point data
2604
+ sortby - name of field to sort rows by
2605
+ sort_key - sorting key function, applied to data points before sorting
2606
+ format - Controls whether or not HTML tables are formatted to match
2607
+ styling options (True or False)
2608
+ """
2609
+ options = self._get_options(kwargs)
2610
+
2611
+ if options["format"]:
2612
+ string = self._get_formatted_latex_string(options)
2613
+ else:
2614
+ string = self._get_simple_latex_string(options)
2615
+ return string
2616
+
2617
+ def _get_simple_latex_string(self, options: OptionsType) -> str:
2618
+ lines: list[str] = []
2619
+
2620
+ wanted_fields = []
2621
+ if options["fields"]:
2622
+ wanted_fields = [
2623
+ field for field in self._field_names if field in options["fields"]
2624
+ ]
2625
+ else:
2626
+ wanted_fields = self._field_names
2627
+
2628
+ alignments = "".join([self._align[field] for field in wanted_fields])
2629
+
2630
+ begin_cmd = f"\\begin{{tabular}}{{{alignments}}}"
2631
+ lines.append(begin_cmd)
2632
+
2633
+ # Headers
2634
+ if options["header"]:
2635
+ lines.append(" & ".join(wanted_fields) + " \\\\")
2636
+
2637
+ # Data
2638
+ rows = self._get_rows(options)
2639
+ formatted_rows = self._format_rows(rows)
2640
+ for row in formatted_rows:
2641
+ wanted_data = [
2642
+ d for f, d in zip(self._field_names, row) if f in wanted_fields
2643
+ ]
2644
+ lines.append(" & ".join(wanted_data) + " \\\\")
2645
+
2646
+ lines.append("\\end{tabular}")
2647
+
2648
+ return "\r\n".join(lines)
2649
+
2650
+ def _get_formatted_latex_string(self, options: OptionsType) -> str:
2651
+ lines: list[str] = []
2652
+
2653
+ wanted_fields: list[str] = []
2654
+ if options["fields"]:
2655
+ wanted_fields = [
2656
+ field for field in self._field_names if field in options["fields"]
2657
+ ]
2658
+ else:
2659
+ wanted_fields = self._field_names
2660
+
2661
+ wanted_alignments = [self._align[field] for field in wanted_fields]
2662
+ if options["border"] and options["vrules"] == VRuleStyle.ALL:
2663
+ alignment_str = "|".join(wanted_alignments)
2664
+ elif not options["border"] and options["preserve_internal_border"]:
2665
+ alignment_str = "|".join(wanted_alignments)
2666
+ else:
2667
+ alignment_str = "".join(wanted_alignments)
2668
+
2669
+ if options["border"] and options["vrules"] in [
2670
+ VRuleStyle.ALL,
2671
+ VRuleStyle.FRAME,
2672
+ ]:
2673
+ alignment_str = "|" + alignment_str + "|"
2674
+
2675
+ begin_cmd = f"\\begin{{tabular}}{{{alignment_str}}}"
2676
+ lines.append(begin_cmd)
2677
+ if options["border"] and options["hrules"] in [
2678
+ HRuleStyle.ALL,
2679
+ HRuleStyle.FRAME,
2680
+ ]:
2681
+ lines.append("\\hline")
2682
+
2683
+ # Headers
2684
+ if options["header"]:
2685
+ lines.append(" & ".join(wanted_fields) + " \\\\")
2686
+ if (options["border"] or options["preserve_internal_border"]) and options[
2687
+ "hrules"
2688
+ ] in [HRuleStyle.ALL, HRuleStyle.HEADER]:
2689
+ lines.append("\\hline")
2690
+
2691
+ # Data
2692
+ rows = self._get_rows(options)
2693
+ formatted_rows = self._format_rows(rows)
2694
+ rows = self._get_rows(options)
2695
+ for row in formatted_rows:
2696
+ wanted_data = [
2697
+ d for f, d in zip(self._field_names, row) if f in wanted_fields
2698
+ ]
2699
+ lines.append(" & ".join(wanted_data) + " \\\\")
2700
+ if options["border"] and options["hrules"] == HRuleStyle.ALL:
2701
+ lines.append("\\hline")
2702
+
2703
+ if options["border"] and options["hrules"] == HRuleStyle.FRAME:
2704
+ lines.append("\\hline")
2705
+
2706
+ lines.append("\\end{tabular}")
2707
+
2708
+ return "\r\n".join(lines)
2709
+
2710
+
2711
+ ##############################
2712
+ # UNICODE WIDTH FUNCTION #
2713
+ ##############################
2714
+
2715
+
2716
+ def _str_block_width(val: str) -> int:
2717
+ import wcwidth # type: ignore[import-untyped]
2718
+
2719
+ return wcwidth.wcswidth(_re.sub("", val))
2720
+
2721
+
2722
+ ##############################
2723
+ # TABLE FACTORIES #
2724
+ ##############################
2725
+
2726
+
2727
+ def from_csv(fp, field_names: Sequence[str] | None = None, **kwargs) -> PrettyTable:
2728
+ import csv
2729
+
2730
+ fmtparams = {}
2731
+ for param in [
2732
+ "delimiter",
2733
+ "doublequote",
2734
+ "escapechar",
2735
+ "lineterminator",
2736
+ "quotechar",
2737
+ "quoting",
2738
+ "skipinitialspace",
2739
+ "strict",
2740
+ ]:
2741
+ if param in kwargs:
2742
+ fmtparams[param] = kwargs.pop(param)
2743
+ if fmtparams:
2744
+ reader = csv.reader(fp, **fmtparams)
2745
+ else:
2746
+ dialect = csv.Sniffer().sniff(fp.read(1024))
2747
+ fp.seek(0)
2748
+ reader = csv.reader(fp, dialect)
2749
+
2750
+ table = PrettyTable(**kwargs)
2751
+ if field_names:
2752
+ table.field_names = field_names
2753
+ else:
2754
+ table.field_names = [x.strip() for x in next(reader)]
2755
+
2756
+ for row in reader:
2757
+ table.add_row([x.strip() for x in row])
2758
+
2759
+ return table
2760
+
2761
+
2762
+ def from_db_cursor(cursor: Cursor, **kwargs) -> PrettyTable | None:
2763
+ if cursor.description:
2764
+ table = PrettyTable(**kwargs)
2765
+ table.field_names = [col[0] for col in cursor.description]
2766
+ for row in cursor.fetchall():
2767
+ table.add_row(row)
2768
+ return table
2769
+ return None
2770
+
2771
+
2772
+ def from_json(json_string: str | bytes, **kwargs) -> PrettyTable:
2773
+ import json
2774
+
2775
+ table = PrettyTable(**kwargs)
2776
+ objects = json.loads(json_string)
2777
+ table.field_names = objects[0]
2778
+ for obj in objects[1:]:
2779
+ row = [obj[key] for key in table.field_names]
2780
+ table.add_row(row)
2781
+ return table
2782
+
2783
+
2784
+ class TableHandler(HTMLParser):
2785
+ def __init__(self, **kwargs) -> None:
2786
+ HTMLParser.__init__(self)
2787
+ self.kwargs = kwargs
2788
+ self.tables: list[PrettyTable] = []
2789
+ self.last_row: list[str] = []
2790
+ self.rows: list[tuple[list[str], bool]] = []
2791
+ self.max_row_width = 0
2792
+ self.active: str | None = None
2793
+ self.last_content = ""
2794
+ self.is_last_row_header = False
2795
+ self.colspan = 0
2796
+
2797
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
2798
+ self.active = tag
2799
+ if tag == "th":
2800
+ self.is_last_row_header = True
2801
+ for key, value in attrs:
2802
+ if key == "colspan":
2803
+ self.colspan = int(value) # type: ignore[arg-type]
2804
+
2805
+ def handle_endtag(self, tag: str) -> None:
2806
+ if tag in ["th", "td"]:
2807
+ stripped_content = self.last_content.strip()
2808
+ self.last_row.append(stripped_content)
2809
+ if self.colspan:
2810
+ for _ in range(1, self.colspan):
2811
+ self.last_row.append("")
2812
+ self.colspan = 0
2813
+
2814
+ if tag == "tr":
2815
+ self.rows.append((self.last_row, self.is_last_row_header))
2816
+ self.max_row_width = max(self.max_row_width, len(self.last_row))
2817
+ self.last_row = []
2818
+ self.is_last_row_header = False
2819
+ if tag == "table":
2820
+ table = self.generate_table(self.rows)
2821
+ self.tables.append(table)
2822
+ self.rows = []
2823
+ self.last_content = " "
2824
+ self.active = None
2825
+
2826
+ def handle_data(self, data: str) -> None:
2827
+ self.last_content += data
2828
+
2829
+ def generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
2830
+ """
2831
+ Generates from a list of rows a PrettyTable object.
2832
+ """
2833
+ table = PrettyTable(**self.kwargs)
2834
+ for row in self.rows:
2835
+ if len(row[0]) < self.max_row_width:
2836
+ appends = self.max_row_width - len(row[0])
2837
+ for i in range(1, appends):
2838
+ row[0].append("-")
2839
+
2840
+ if row[1]:
2841
+ self.make_fields_unique(row[0])
2842
+ table.field_names = row[0]
2843
+ else:
2844
+ table.add_row(row[0])
2845
+ return table
2846
+
2847
+ def make_fields_unique(self, fields: list[str]) -> None:
2848
+ """
2849
+ iterates over the row and make each field unique
2850
+ """
2851
+ for i in range(0, len(fields)):
2852
+ for j in range(i + 1, len(fields)):
2853
+ if fields[i] == fields[j]:
2854
+ fields[j] += "'"
2855
+
2856
+
2857
+ def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
2858
+ """
2859
+ Generates a list of PrettyTables from a string of HTML code. Each <table> in
2860
+ the HTML becomes one PrettyTable object.
2861
+ """
2862
+
2863
+ parser = TableHandler(**kwargs)
2864
+ parser.feed(html_code)
2865
+ return parser.tables
2866
+
2867
+
2868
+ def from_html_one(html_code: str, **kwargs) -> PrettyTable:
2869
+ """
2870
+ Generates a PrettyTable from a string of HTML code which contains only a
2871
+ single <table>
2872
+ """
2873
+
2874
+ tables = from_html(html_code, **kwargs)
2875
+ try:
2876
+ assert len(tables) == 1
2877
+ except AssertionError:
2878
+ msg = "More than one <table> in provided HTML code. Use from_html instead."
2879
+ raise ValueError(msg)
2880
+ return tables[0]
2881
+
2882
+
2883
+ def _warn_deprecation(name: str, module_globals: dict[str, Any]) -> Any:
2884
+ if (val := module_globals.get(f"_DEPRECATED_{name}")) is None:
2885
+ msg = f"module '{__name__}' has no attribute '{name}'"
2886
+ raise AttributeError(msg)
2887
+ module_globals[name] = val
2888
+ if name in {"FRAME", "ALL", "NONE", "HEADER"}:
2889
+ msg = (
2890
+ f"the '{name}' constant is deprecated, "
2891
+ "use the 'HRuleStyle' and 'VRuleStyle' enums instead"
2892
+ )
2893
+ else:
2894
+ msg = f"the '{name}' constant is deprecated, use the 'TableStyle' enum instead"
2895
+ warnings.warn(msg, DeprecationWarning, stacklevel=3)
2896
+ return val
2897
+
2898
+
2899
+ def __getattr__(name: str) -> Any:
2900
+ return _warn_deprecation(name, module_globals=globals())