Skip to content

refactor: Annotate value type with SupportsStr protocol #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,33 +149,35 @@ See a list of all preset styles [here](https://table2ascii.readthedocs.io/en/lat

All parameters are optional.

| Option | Type | Default | Description |
| :-----------------: | :---------------: | :----------: | :----------------------------------------------------------------------------------------: |
| `header` | `List[str]` | `None` | First row of table seperated by header row seperator |
| `body` | `List[List[str]]` | `None` | List of rows for the main section of the table |
| `footer` | `List[str]` | `None` | Last row of table seperated by header row seperator |
| `column_widths` | `List[int]` | automatic | List of column widths in characters for each column |
| `alignments` | `List[int]` | all centered | Alignments for each column<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column |
| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column |
| Option | Type | Default | Description |
| :-----------------: | :-------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
| `header` | `List[Any]` | `None` | First table row seperated by header row seperator. Values should support `str()`. |
| `body` | `List[List[Any]]` | `None` | List of rows for the main section of the table. Values should support `str()`. |
| `footer` | `List[Any]` | `None` | Last table row seperated by header row seperator. Values should support `str()`. |
| `column_widths` | `List[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
| `alignments` | `List[Alignment]` | `None` (all centered) | Column alignments<br/>(ex. `[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]`) |
| `style` | `TableStyle` | `double_thin_compact` | Table style to use for the table |
| `first_col_heading` | `bool` | `False` | Whether to add a heading column seperator after the first column |
| `last_col_heading` | `bool` | `False` | Whether to add a heading column seperator before the last column |

See the [API Reference](https://table2ascii.readthedocs.io/en/latest/api.html) for more info.

## 👨‍🎨 Use cases

### Discord messages and embeds

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

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

### Terminal outputs

* Tables display nicely whenever monospace fonts are fully supported
* Tables make terminal outputs look more professional
- Tables display nicely whenever monospace fonts are fully supported
- Tables make terminal outputs look more professional

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


## 🤗 Contributing

Contributions are welcome!
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
typing_extensions>=4.0.0,<5
23 changes: 23 additions & 0 deletions table2ascii/annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import abstractmethod
from typing import TYPE_CHECKING

try:
# Python 3.8+
from typing import Protocol, runtime_checkable
except ImportError:
# Python 3.7
from typing_extensions import Protocol, runtime_checkable

if TYPE_CHECKING:
from typing import Protocol


@runtime_checkable
class SupportsStr(Protocol):
"""An ABC with one abstract method __str__."""

__slots__ = ()

@abstractmethod
def __str__(self) -> str:
pass
35 changes: 19 additions & 16 deletions table2ascii/table_to_ascii.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from math import ceil, floor
from typing import Any, Callable, List, Optional, Union
from typing import Callable, List, Optional, Union

from .alignment import Alignment
from .annotations import SupportsStr
from .options import Options
from .preset_style import PresetStyle
from .table_style import TableStyle
Expand All @@ -12,9 +13,9 @@ class TableToAscii:

def __init__(
self,
header: Optional[List[Any]],
body: Optional[List[List[Any]]],
footer: Optional[List[Any]],
header: Optional[List[SupportsStr]],
body: Optional[List[List[SupportsStr]]],
footer: Optional[List[SupportsStr]],
options: Options,
):
"""
Expand Down Expand Up @@ -103,7 +104,9 @@ def widest_line(text: str) -> int:
# get the width necessary for each column
for i in range(self.__columns):
# col_widest returns the width of the widest line in the ith cell of a given list
col_widest: Callable[[List[Any], int], int] = lambda row, i=i: widest_line(str(row[i]))
col_widest: Callable[[List[SupportsStr], int], int] = lambda row, i=i: widest_line(
str(row[i])
)
# number of characters in column of i of header, each body row, and footer
header_size = col_widest(self.__header) if self.__header else 0
body_size = map(col_widest, self.__body) if self.__body else [0]
Expand All @@ -112,7 +115,7 @@ def widest_line(text: str) -> int:
column_widths.append(max(header_size, *body_size, footer_size) + 2)
return column_widths

def __pad(self, cell_value: Any, width: int, alignment: Alignment) -> str:
def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> str:
"""
Pad a string of text to a given width with specified alignment

Expand Down Expand Up @@ -258,7 +261,7 @@ def __heading_sep_to_ascii(self) -> str:
filler=self.__style.heading_row_sep,
)

def __body_to_ascii(self, body: List[List[Any]]) -> str:
def __body_to_ascii(self, body: List[List[SupportsStr]]) -> str:
"""
Assembles the body of the ascii table

Expand Down Expand Up @@ -310,9 +313,9 @@ def to_ascii(self) -> str:


def table2ascii(
header: Optional[List[Any]] = None,
body: Optional[List[List[Any]]] = None,
footer: Optional[List[Any]] = None,
header: Optional[List[SupportsStr]] = None,
body: Optional[List[List[SupportsStr]]] = None,
footer: Optional[List[SupportsStr]] = None,
*,
first_col_heading: bool = False,
last_col_heading: bool = False,
Expand All @@ -324,12 +327,12 @@ def table2ascii(
Convert a 2D Python table to ASCII text

Args:
header: List of column values in the table's header row. If not specified,
the table will not have a header row.
body: 2-dimensional list of values in the table's body. If not specified,
the table will not have a body.
footer: List of column values in the table's footer row. If not specified,
the table will not have a footer row.
header: List of column values in the table's header row. All values should be :class:`str`
or support :class:`str` conversion. If not specified, the table will not have a header row.
body: 2-dimensional list of values in the table's body. All values should be :class:`str`
or support :class:`str` conversion. If not specified, the table will not have a body.
footer: List of column values in the table's footer row. All values should be :class:`str`
or support :class:`str` conversion. If not specified, the table will not have a footer row.
first_col_heading: Whether to add a header column separator after the first column.
Defaults to :py:obj:`False`.
last_col_heading: Whether to add a header column separator before the last column.
Expand Down
23 changes: 23 additions & 0 deletions tests/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,29 @@ def test_numeric_data():
assert text == expected


def test_stringifiable_classes():
class Foo:
def __str__(self):
return "Foo"

text = t2a(
header=[1, Foo(), None],
body=[[1, Foo(), None]],
footer=[1, Foo(), None],
first_col_heading=True,
)
expected = (
"╔═══╦════════════╗\n"
"║ 1 ║ Foo None ║\n"
"╟───╫────────────╢\n"
"║ 1 ║ Foo None ║\n"
"╟───╫────────────╢\n"
"║ 1 ║ Foo None ║\n"
"╚═══╩════════════╝"
)
assert text == expected


def test_multiline_cells():
text = t2a(
header=["Multiline\nHeader\nCell", "G", "Two\nLines", "R", "S"],
Expand Down