Skip to content

Commit 71f990f

Browse files
committed
refactor: Use less restrictive types
1 parent 36e74b8 commit 71f990f

File tree

4 files changed

+78
-66
lines changed

4 files changed

+78
-66
lines changed

Diff for: README.md

+16-14
Original file line numberDiff line numberDiff line change
@@ -195,20 +195,22 @@ print(output)
195195

196196
## ⚙️ Options
197197

198-
All parameters are optional.
199-
200-
| Option | Type | Default | Description |
201-
| :-----------------: | :-------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
202-
| `header` | `List[Any]` | `None` | First table row seperated by header row separator. Values should support `str()` |
203-
| `body` | `List[List[Any]]` | `None` | List of rows for the main section of the table. Values should support `str()` |
204-
| `footer` | `List[Any]` | `None` | Last table row seperated by header row separator. Values should support `str()` |
205-
| `column_widths` | `List[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
206-
| `alignments` | `List[Alignment]` | `None` (all centered) | Column alignments<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
207-
| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* |
208-
| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column |
209-
| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column |
210-
| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border |
211-
| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width |
198+
All parameters are optional. At least one of `header`, `body`, and `footer` must be provided.
199+
200+
Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.html#table2ascii) for more information.
201+
202+
| Option | Type | Default | Description |
203+
| :-----------------: | :----------------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
204+
| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` |
205+
| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` |
206+
| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` |
207+
| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
208+
| `alignments` | `Sequence[Alignment]` | `None` (all centered) | Column alignments<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
209+
| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table\* |
210+
| `first_col_heading` | `bool` | `False` | Whether to add a heading column separator after the first column |
211+
| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column |
212+
| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border |
213+
| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width |
212214

213215
[wcwidth]: https://pypi.org/project/wcwidth/
214216

Diff for: table2ascii/exceptions.py

+21-20
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from __future__ import annotations
2+
3+
from collections.abc import Sequence
24
from typing import Any
35

46
from .alignment import Alignment
5-
67
from .annotations import SupportsStr
78

89

910
class Table2AsciiError(Exception):
1011
"""Base class for all table2ascii exceptions"""
1112

12-
def _message(self):
13+
def _message(self) -> str:
1314
"""Return the error message"""
1415
raise NotImplementedError
1516

@@ -39,16 +40,16 @@ class FooterColumnCountMismatchError(ColumnCountMismatchError):
3940
This class is a subclass of :class:`ColumnCountMismatchError`.
4041
4142
Attributes:
42-
footer (list[SupportsStr]): The footer that caused the error
43+
footer (Sequence[SupportsStr]): The footer that caused the error
4344
expected_columns (int): The number of columns that were expected
4445
"""
4546

46-
def __init__(self, footer: list[SupportsStr], expected_columns: int):
47+
def __init__(self, footer: Sequence[SupportsStr], expected_columns: int):
4748
self.footer = footer
4849
self.expected_columns = expected_columns
4950
super().__init__(self._message())
5051

51-
def _message(self):
52+
def _message(self) -> str:
5253
return (
5354
f"Footer column count mismatch: {len(self.footer)} columns "
5455
f"found, expected {self.expected_columns}."
@@ -62,20 +63,20 @@ class BodyColumnCountMismatchError(ColumnCountMismatchError):
6263
This class is a subclass of :class:`ColumnCountMismatchError`.
6364
6465
Attributes:
65-
body (list[list[SupportsStr]]): The body that caused the error
66+
body (Sequence[Sequence[SupportsStr]]): The body that caused the error
6667
expected_columns (int): The number of columns that were expected
67-
first_invalid_row (list[SupportsStr]): The first row with an invalid column count
68+
first_invalid_row (Sequence[SupportsStr]): The first row with an invalid column count
6869
"""
6970

70-
def __init__(self, body: list[list[SupportsStr]], expected_columns: int):
71+
def __init__(self, body: Sequence[Sequence[SupportsStr]], expected_columns: int):
7172
self.body = body
7273
self.expected_columns = expected_columns
7374
self.first_invalid_row = next(
7475
(row for row in self.body if len(row) != self.expected_columns)
7576
)
7677
super().__init__(self._message())
7778

78-
def _message(self):
79+
def _message(self) -> str:
7980
return (
8081
f"Body column count mismatch: A row with {len(self.first_invalid_row)} "
8182
f"columns was found, expected {self.expected_columns}."
@@ -89,16 +90,16 @@ class AlignmentCountMismatchError(ColumnCountMismatchError):
8990
This class is a subclass of :class:`ColumnCountMismatchError`.
9091
9192
Attributes:
92-
alignments (list[Alignment]): The alignments that caused the error
93+
alignments (Sequence[Alignment]): The alignments that caused the error
9394
expected_columns (int): The number of columns that were expected
9495
"""
9596

96-
def __init__(self, alignments: list[Alignment], expected_columns: int):
97+
def __init__(self, alignments: Sequence[Alignment], expected_columns: int):
9798
self.alignments = alignments
9899
self.expected_columns = expected_columns
99100
super().__init__(self._message())
100101

101-
def _message(self):
102+
def _message(self) -> str:
102103
return (
103104
f"Alignment count mismatch: {len(self.alignments)} alignments "
104105
f"found, expected {self.expected_columns}."
@@ -112,16 +113,16 @@ class ColumnWidthsCountMismatchError(ColumnCountMismatchError):
112113
This class is a subclass of :class:`ColumnCountMismatchError`.
113114
114115
Attributes:
115-
column_widths (list[Optional[int]]): The column widths that caused the error
116+
column_widths (Sequence[Optional[int]]): The column widths that caused the error
116117
expected_columns (int): The number of columns that were expected
117118
"""
118119

119-
def __init__(self, column_widths: list[int | None], expected_columns: int):
120+
def __init__(self, column_widths: Sequence[int | None], expected_columns: int):
120121
self.column_widths = column_widths
121122
self.expected_columns = expected_columns
122123
super().__init__(self._message())
123124

124-
def _message(self):
125+
def _message(self) -> str:
125126
return (
126127
f"Column widths count mismatch: {len(self.column_widths)} column widths "
127128
f"found, expected {self.expected_columns}."
@@ -154,7 +155,7 @@ def __init__(self, padding: int):
154155
self.padding = padding
155156
super().__init__(self._message())
156157

157-
def _message(self):
158+
def _message(self) -> str:
158159
return f"Invalid cell padding: {self.padding} is not a positive integer."
159160

160161

@@ -176,7 +177,7 @@ def __init__(self, column_index: int, column_width: int, min_width: int):
176177
self.min_width = min_width
177178
super().__init__(self._message())
178179

179-
def _message(self):
180+
def _message(self) -> str:
180181
return (
181182
f"Column width too small: The column width for column index {self.column_index} "
182183
f" of `column_widths` is {self.column_width}, but the minimum width "
@@ -197,7 +198,7 @@ def __init__(self, alignment: Any):
197198
self.alignment = alignment
198199
super().__init__(self._message())
199200

200-
def _message(self):
201+
def _message(self) -> str:
201202
return (
202203
f"Invalid alignment: {self.alignment!r} is not a valid alignment. "
203204
f"Valid alignments are: {', '.join(a.__repr__() for a in Alignment)}"
@@ -221,7 +222,7 @@ def __init__(self, string: str, max_characters: int):
221222
self.max_characters = max_characters
222223
super().__init__(self._message())
223224

224-
def _message(self):
225+
def _message(self) -> str:
225226
return (
226227
f"Too many characters for table style: {len(self.string)} characters "
227228
f"found, but the maximum number of characters allowed is {self.max_characters}."
@@ -247,7 +248,7 @@ def __init__(self, string: str, max_characters: int):
247248
self.max_characters = max_characters
248249
super().__init__(self._message())
249250

250-
def _message(self):
251+
def _message(self) -> str:
251252
return (
252253
f"Too few characters for table style: {len(self.string)} characters "
253254
f"found, but table styles can accept {self.max_characters} characters. "

Diff for: table2ascii/options.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from collections.abc import Sequence
34
from dataclasses import dataclass
45

56
from .alignment import Alignment
@@ -17,8 +18,8 @@ class Options:
1718

1819
first_col_heading: bool
1920
last_col_heading: bool
20-
column_widths: list[int | None] | None
21-
alignments: list[Alignment] | None
21+
column_widths: Sequence[int | None] | None
22+
alignments: Sequence[Alignment] | None
2223
cell_padding: int
2324
style: TableStyle
2425
use_wcwidth: bool

Diff for: table2ascii/table_to_ascii.py

+38-30
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import textwrap
44
from math import ceil, floor
5+
from collections.abc import Sequence
56

67
from wcwidth import wcswidth
78

@@ -15,6 +16,7 @@
1516
FooterColumnCountMismatchError,
1617
InvalidAlignmentError,
1718
InvalidCellPaddingError,
19+
NoHeaderBodyOrFooterError,
1820
)
1921
from .merge import Merge
2022
from .options import Options
@@ -27,9 +29,9 @@ class TableToAscii:
2729

2830
def __init__(
2931
self,
30-
header: list[SupportsStr] | None,
31-
body: list[list[SupportsStr]] | None,
32-
footer: list[SupportsStr] | None,
32+
header: Sequence[SupportsStr] | None,
33+
body: Sequence[Sequence[SupportsStr]] | None,
34+
footer: Sequence[SupportsStr] | None,
3335
options: Options,
3436
):
3537
"""Validate arguments and initialize fields
@@ -41,9 +43,9 @@ def __init__(
4143
options: The options for the table
4244
"""
4345
# initialize fields
44-
self.__header = header
45-
self.__body = body
46-
self.__footer = footer
46+
self.__header = list(header) if header else None
47+
self.__body = list([list(row) for row in body]) if body else None
48+
self.__footer = list(footer) if footer else None
4749
self.__style = options.style
4850
self.__first_col_heading = options.first_col_heading
4951
self.__last_col_heading = options.last_col_heading
@@ -60,6 +62,10 @@ def __init__(
6062
if body and any(len(row) != self.__columns for row in body):
6163
raise BodyColumnCountMismatchError(body, self.__columns)
6264

65+
# check that at least one of header, body, or footer is not None
66+
if not header and not body and not footer:
67+
raise NoHeaderBodyOrFooterError()
68+
6369
# calculate or use given column widths
6470
self.__column_widths = self.__calculate_column_widths(options.column_widths)
6571

@@ -103,7 +109,7 @@ def widest_line(value: SupportsStr) -> int:
103109
text = str(value)
104110
return max(self.__str_width(line) for line in text.splitlines()) if len(text) else 0
105111

106-
def get_column_width(row: list[SupportsStr], column: int) -> int:
112+
def get_column_width(row: Sequence[SupportsStr], column: int) -> int:
107113
"""Get the width of a cell in a column"""
108114
value = row[column]
109115
next_value = row[column + 1] if column < self.__columns - 1 else None
@@ -122,7 +128,9 @@ def get_column_width(row: list[SupportsStr], column: int) -> int:
122128
column_widths.append(max(header_size, body_size, footer_size) + self.__cell_padding * 2)
123129
return column_widths
124130

125-
def __calculate_column_widths(self, user_column_widths: list[int | None] | None) -> list[int]:
131+
def __calculate_column_widths(
132+
self, user_column_widths: Sequence[int | None] | None
133+
) -> list[int]:
126134
"""Calculate the width of each column in the table based on the cell values and provided column widths.
127135
128136
Args:
@@ -187,7 +195,7 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
187195
raise InvalidAlignmentError(alignment)
188196

189197
def __wrap_long_lines_in_merged_cells(
190-
self, row: list[SupportsStr], column_separator: str
198+
self, row: Sequence[SupportsStr], column_separator: str
191199
) -> list[SupportsStr]:
192200
"""Wrap long lines in merged cells to the width of the merged cell
193201
@@ -219,9 +227,9 @@ def __row_to_ascii(
219227
heading_col_sep: str,
220228
column_separator: str,
221229
right_edge: str,
222-
filler: str | list[SupportsStr],
223-
previous_content_row: list[SupportsStr] | None = None,
224-
next_content_row: list[SupportsStr] | None = None,
230+
filler: str | Sequence[SupportsStr],
231+
previous_content_row: Sequence[SupportsStr] | None = None,
232+
next_content_row: Sequence[SupportsStr] | None = None,
225233
top_tee: str | None = None,
226234
bottom_tee: str | None = None,
227235
heading_col_top_tee: str | None = None,
@@ -266,9 +274,9 @@ def __line_in_row_to_ascii(
266274
heading_col_sep: str,
267275
column_separator: str,
268276
right_edge: str,
269-
filler: str | list[SupportsStr],
270-
previous_content_row: list[SupportsStr] | None = None,
271-
next_content_row: list[SupportsStr] | None = None,
277+
filler: str | Sequence[SupportsStr],
278+
previous_content_row: Sequence[SupportsStr] | None = None,
279+
next_content_row: Sequence[SupportsStr] | None = None,
272280
top_tee: str | None = None,
273281
bottom_tee: str | None = None,
274282
heading_col_top_tee: str | None = None,
@@ -306,9 +314,9 @@ def __line_in_cell_column_to_ascii(
306314
heading_col_sep: str,
307315
column_separator: str,
308316
right_edge: str,
309-
filler: str | list[SupportsStr],
310-
previous_content_row: list[SupportsStr] | None = None,
311-
next_content_row: list[SupportsStr] | None = None,
317+
filler: str | Sequence[SupportsStr],
318+
previous_content_row: Sequence[SupportsStr] | None = None,
319+
next_content_row: Sequence[SupportsStr] | None = None,
312320
top_tee: str | None = None,
313321
bottom_tee: str | None = None,
314322
heading_col_top_tee: str | None = None,
@@ -373,7 +381,7 @@ def __line_in_cell_column_to_ascii(
373381
return output + sep
374382

375383
def __get_padded_cell_line_content(
376-
self, line_index: int, col_index: int, column_separator: str, filler: list[SupportsStr]
384+
self, line_index: int, col_index: int, column_separator: str, filler: Sequence[SupportsStr]
377385
) -> str:
378386
# If this is a merge cell, merge with the previous column
379387
if filler[col_index] is Merge.LEFT:
@@ -437,7 +445,7 @@ def __bottom_edge_to_ascii(self) -> str:
437445
heading_col_bottom_tee=self.__style.heading_col_bottom_tee,
438446
)
439447

440-
def __content_row_to_ascii(self, row: list[SupportsStr]) -> str:
448+
def __content_row_to_ascii(self, row: Sequence[SupportsStr]) -> str:
441449
"""Assembles a row of cell values into a single line of the ascii table
442450
443451
Returns:
@@ -453,8 +461,8 @@ def __content_row_to_ascii(self, row: list[SupportsStr]) -> str:
453461

454462
def __heading_sep_to_ascii(
455463
self,
456-
previous_content_row: list[SupportsStr] | None = None,
457-
next_content_row: list[SupportsStr] | None = None,
464+
previous_content_row: Sequence[SupportsStr] | None = None,
465+
next_content_row: Sequence[SupportsStr] | None = None,
458466
) -> str:
459467
"""Assembles the separator below the header or above footer of the ascii table
460468
@@ -475,7 +483,7 @@ def __heading_sep_to_ascii(
475483
heading_col_bottom_tee=self.__style.heading_col_heading_row_bottom_tee,
476484
)
477485

478-
def __body_to_ascii(self, body: list[list[SupportsStr]]) -> str:
486+
def __body_to_ascii(self, body: Sequence[Sequence[SupportsStr]]) -> str:
479487
"""Assembles the body of the ascii table
480488
481489
Returns:
@@ -551,14 +559,14 @@ def to_ascii(self) -> str:
551559

552560

553561
def table2ascii(
554-
header: list[SupportsStr] | None = None,
555-
body: list[list[SupportsStr]] | None = None,
556-
footer: list[SupportsStr] | None = None,
562+
header: Sequence[SupportsStr] | None = None,
563+
body: Sequence[Sequence[SupportsStr]] | None = None,
564+
footer: Sequence[SupportsStr] | None = None,
557565
*,
558566
first_col_heading: bool = False,
559567
last_col_heading: bool = False,
560-
column_widths: list[int | None] | None = None,
561-
alignments: list[Alignment] | None = None,
568+
column_widths: Sequence[int | None] | None = None,
569+
alignments: Sequence[Alignment] | None = None,
562570
cell_padding: int = 1,
563571
style: TableStyle = PresetStyle.double_thin_compact,
564572
use_wcwidth: bool = True,
@@ -581,8 +589,8 @@ def table2ascii(
581589
Defaults to :py:obj:`False`.
582590
column_widths: List of widths in characters for each column. Any value of :py:obj:`None`
583591
indicates that the column width should be determined automatically. If :py:obj:`None`
584-
is passed instead of a :class:`list`, all columns will be automatically sized.
585-
Defaults to :py:obj:`None`.
592+
is passed instead of a :class:`~collections.abc.Sequence`, all columns will be automatically
593+
sized. Defaults to :py:obj:`None`.
586594
alignments: List of alignments for each column
587595
(ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to
588596
:py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`.

0 commit comments

Comments
 (0)