Skip to content

Commit 3c2cdf6

Browse files
authored
refactor: Annotate value type with SupportsStr protocol (#44)
1 parent f7f310f commit 3c2cdf6

File tree

5 files changed

+82
-30
lines changed

5 files changed

+82
-30
lines changed

README.md

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,33 +149,35 @@ See a list of all preset styles [here](https://table2ascii.readthedocs.io/en/lat
149149

150150
All parameters are optional.
151151

152-
| Option | Type | Default | Description |
153-
| :-----------------: | :---------------: | :----------: | :----------------------------------------------------------------------------------------: |
154-
| `header` | `List[str]` | `None` | First row of table seperated by header row seperator |
155-
| `body` | `List[List[str]]` | `None` | List of rows for the main section of the table |
156-
| `footer` | `List[str]` | `None` | Last row of table seperated by header row seperator |
157-
| `column_widths` | `List[int]` | automatic | List of column widths in characters for each column |
158-
| `alignments` | `List[int]` | all centered | Alignments for each column<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
159-
| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column |
160-
| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column |
152+
| Option | Type | Default | Description |
153+
| :-----------------: | :-------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
154+
| `header` | `List[Any]` | `None` | First table row seperated by header row seperator. Values should support `str()`. |
155+
| `body` | `List[List[Any]]` | `None` | List of rows for the main section of the table. Values should support `str()`. |
156+
| `footer` | `List[Any]` | `None` | Last table row seperated by header row seperator. Values should support `str()`. |
157+
| `column_widths` | `List[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
158+
| `alignments` | `List[Alignment]` | `None` (all centered) | Column alignments<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
159+
| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table |
160+
| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column |
161+
| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column |
162+
163+
See the [API Reference](https://table2ascii.readthedocs.io/en/latest/api.html) for more info.
161164

162165
## 👨‍🎨 Use cases
163166

164167
### Discord messages and embeds
165168

166-
* Display tables nicely inside markdown codeblocks on Discord
167-
* Useful for making Discord bots with [Discord.py](https://github.com/Rapptz/discord.py)
169+
- Display tables nicely inside markdown code blocks on Discord
170+
- Useful for making Discord bots with [Discord.py](https://github.com/Rapptz/discord.py)
168171

169172
![image](https://user-images.githubusercontent.com/20955511/116203248-2973c600-a744-11eb-97d8-4b75ed2845c9.png)
170173

171174
### Terminal outputs
172175

173-
* Tables display nicely whenever monospace fonts are fully supported
174-
* Tables make terminal outputs look more professional
176+
- Tables display nicely whenever monospace fonts are fully supported
177+
- Tables make terminal outputs look more professional
175178

176179
![image](https://user-images.githubusercontent.com/20955511/116204490-802dcf80-a745-11eb-9b4a-7cef49f23958.png)
177180

178-
179181
## 🤗 Contributing
180182

181183
Contributions are welcome!

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
typing_extensions>=4.0.0,<5

table2ascii/annotations.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from abc import abstractmethod
2+
from typing import TYPE_CHECKING
3+
4+
try:
5+
# Python 3.8+
6+
from typing import Protocol, runtime_checkable
7+
except ImportError:
8+
# Python 3.7
9+
from typing_extensions import Protocol, runtime_checkable
10+
11+
if TYPE_CHECKING:
12+
from typing import Protocol
13+
14+
15+
@runtime_checkable
16+
class SupportsStr(Protocol):
17+
"""An ABC with one abstract method __str__."""
18+
19+
__slots__ = ()
20+
21+
@abstractmethod
22+
def __str__(self) -> str:
23+
pass

table2ascii/table_to_ascii.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from math import ceil, floor
2-
from typing import Any, Callable, List, Optional, Union
2+
from typing import Callable, List, Optional, Union
33

44
from .alignment import Alignment
5+
from .annotations import SupportsStr
56
from .options import Options
67
from .preset_style import PresetStyle
78
from .table_style import TableStyle
@@ -12,9 +13,9 @@ class TableToAscii:
1213

1314
def __init__(
1415
self,
15-
header: Optional[List[Any]],
16-
body: Optional[List[List[Any]]],
17-
footer: Optional[List[Any]],
16+
header: Optional[List[SupportsStr]],
17+
body: Optional[List[List[SupportsStr]]],
18+
footer: Optional[List[SupportsStr]],
1819
options: Options,
1920
):
2021
"""
@@ -103,7 +104,9 @@ def widest_line(text: str) -> int:
103104
# get the width necessary for each column
104105
for i in range(self.__columns):
105106
# col_widest returns the width of the widest line in the ith cell of a given list
106-
col_widest: Callable[[List[Any], int], int] = lambda row, i=i: widest_line(str(row[i]))
107+
col_widest: Callable[[List[SupportsStr], int], int] = lambda row, i=i: widest_line(
108+
str(row[i])
109+
)
107110
# number of characters in column of i of header, each body row, and footer
108111
header_size = col_widest(self.__header) if self.__header else 0
109112
body_size = map(col_widest, self.__body) if self.__body else [0]
@@ -112,7 +115,7 @@ def widest_line(text: str) -> int:
112115
column_widths.append(max(header_size, *body_size, footer_size) + 2)
113116
return column_widths
114117

115-
def __pad(self, cell_value: Any, width: int, alignment: Alignment) -> str:
118+
def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> str:
116119
"""
117120
Pad a string of text to a given width with specified alignment
118121
@@ -258,7 +261,7 @@ def __heading_sep_to_ascii(self) -> str:
258261
filler=self.__style.heading_row_sep,
259262
)
260263

261-
def __body_to_ascii(self, body: List[List[Any]]) -> str:
264+
def __body_to_ascii(self, body: List[List[SupportsStr]]) -> str:
262265
"""
263266
Assembles the body of the ascii table
264267
@@ -310,9 +313,9 @@ def to_ascii(self) -> str:
310313

311314

312315
def table2ascii(
313-
header: Optional[List[Any]] = None,
314-
body: Optional[List[List[Any]]] = None,
315-
footer: Optional[List[Any]] = None,
316+
header: Optional[List[SupportsStr]] = None,
317+
body: Optional[List[List[SupportsStr]]] = None,
318+
footer: Optional[List[SupportsStr]] = None,
316319
*,
317320
first_col_heading: bool = False,
318321
last_col_heading: bool = False,
@@ -324,12 +327,12 @@ def table2ascii(
324327
Convert a 2D Python table to ASCII text
325328
326329
Args:
327-
header: List of column values in the table's header row. If not specified,
328-
the table will not have a header row.
329-
body: 2-dimensional list of values in the table's body. If not specified,
330-
the table will not have a body.
331-
footer: List of column values in the table's footer row. If not specified,
332-
the table will not have a footer row.
330+
header: List of column values in the table's header row. All values should be :class:`str`
331+
or support :class:`str` conversion. If not specified, the table will not have a header row.
332+
body: 2-dimensional list of values in the table's body. All values should be :class:`str`
333+
or support :class:`str` conversion. If not specified, the table will not have a body.
334+
footer: List of column values in the table's footer row. All values should be :class:`str`
335+
or support :class:`str` conversion. If not specified, the table will not have a footer row.
333336
first_col_heading: Whether to add a header column separator after the first column.
334337
Defaults to :py:obj:`False`.
335338
last_col_heading: Whether to add a header column separator before the last column.

tests/test_convert.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,29 @@ def test_numeric_data():
201201
assert text == expected
202202

203203

204+
def test_stringifiable_classes():
205+
class Foo:
206+
def __str__(self):
207+
return "Foo"
208+
209+
text = t2a(
210+
header=[1, Foo(), None],
211+
body=[[1, Foo(), None]],
212+
footer=[1, Foo(), None],
213+
first_col_heading=True,
214+
)
215+
expected = (
216+
"╔═══╦════════════╗\n"
217+
"║ 1 ║ Foo None ║\n"
218+
"╟───╫────────────╢\n"
219+
"║ 1 ║ Foo None ║\n"
220+
"╟───╫────────────╢\n"
221+
"║ 1 ║ Foo None ║\n"
222+
"╚═══╩════════════╝"
223+
)
224+
assert text == expected
225+
226+
204227
def test_multiline_cells():
205228
text = t2a(
206229
header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"],

0 commit comments

Comments
 (0)