diff --git a/README.md b/README.md index e6ec014..d64cc09 100644 --- a/README.md +++ b/README.md @@ -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
(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
(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! diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e1631f0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +typing_extensions>=4.0.0,<5 \ No newline at end of file diff --git a/table2ascii/annotations.py b/table2ascii/annotations.py new file mode 100644 index 0000000..cb78bdd --- /dev/null +++ b/table2ascii/annotations.py @@ -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 diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index b0f07bf..7b8ab81 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -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 @@ -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, ): """ @@ -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] @@ -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 @@ -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 @@ -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, @@ -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. diff --git a/tests/test_convert.py b/tests/test_convert.py index 5c0653a..3e7a045 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -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"],