diff --git a/README.md b/README.md
index 5689715..b5b50e2 100644
--- a/README.md
+++ b/README.md
@@ -195,20 +195,22 @@ print(output)
## ⚙️ Options
-All parameters are optional.
-
-| Option | Type | Default | Description |
-| :-----------------: | :-------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
-| `header` | `List[Any]` | `None` | First table row seperated by header row separator. 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 separator. 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 separator after the first column |
-| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column |
-| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border |
-| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width |
+All parameters are optional. At least one of `header`, `body`, and `footer` must be provided.
+
+Refer to the [documentation](https://table2ascii.readthedocs.io/en/stable/api.html#table2ascii) for more information.
+
+| Option | Type | Default | Description |
+| :-----------------: | :----------------------------: | :-------------------: | :-------------------------------------------------------------------------------: |
+| `header` | `Sequence[SupportsStr]` | `None` | First table row seperated by header row separator. Values should support `str()` |
+| `body` | `Sequence[Sequence[Sequence]]` | `None` | 2D List of rows for the main section of the table. Values should support `str()` |
+| `footer` | `Sequence[Sequence]` | `None` | Last table row seperated by header row separator. Values should support `str()` |
+| `column_widths` | `Sequence[Optional[int]]` | `None` (automatic) | List of column widths in characters for each column |
+| `alignments` | `Sequence[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 separator after the first column |
+| `last_col_heading` | `bool` | `False` | Whether to add a heading column separator before the last column |
+| `cell_padding` | `int` | `1` | The minimum number of spaces to add between the cell content and the cell border |
+| `use_wcwidth` | `bool` | `True` | Whether to use [wcwidth][wcwidth] instead of `len()` to calculate cell width |
[wcwidth]: https://pypi.org/project/wcwidth/
diff --git a/docs/source/generate_style_list.py b/docs/source/generate_style_list.py
index 633b225..f71fdbe 100644
--- a/docs/source/generate_style_list.py
+++ b/docs/source/generate_style_list.py
@@ -4,12 +4,12 @@
from table2ascii import PresetStyle, table2ascii
-def indent_all_lines(text, number_of_spaces=3):
+def indent_all_lines(text: str, number_of_spaces: int = 3) -> str:
"""Indent all lines in a string by a certain number of spaces"""
return "\n".join(number_of_spaces * " " + line for line in text.split("\n"))
-def generate_style_list():
+def generate_style_list() -> str:
"""Generate README.rst the style list"""
# get attributes in PresetStyle
attribute_names = [attr for attr in dir(PresetStyle) if not attr.startswith("__")]
@@ -43,7 +43,7 @@ def generate_style_list():
return f"{heading}\n\n{table_of_contents}\n{style_list}"
-def write_to_file(filename, content):
+def write_to_file(filename: str, content: str) -> None:
"""Write content to filename"""
with open(filename, "w") as f:
f.write(content)
diff --git a/table2ascii/exceptions.py b/table2ascii/exceptions.py
index 4af1a26..86be92a 100644
--- a/table2ascii/exceptions.py
+++ b/table2ascii/exceptions.py
@@ -1,15 +1,16 @@
from __future__ import annotations
+
+from collections.abc import Sequence
from typing import Any
from .alignment import Alignment
-
from .annotations import SupportsStr
class Table2AsciiError(Exception):
"""Base class for all table2ascii exceptions"""
- def _message(self):
+ def _message(self) -> str:
"""Return the error message"""
raise NotImplementedError
@@ -39,16 +40,16 @@ class FooterColumnCountMismatchError(ColumnCountMismatchError):
This class is a subclass of :class:`ColumnCountMismatchError`.
Attributes:
- footer (list[SupportsStr]): The footer that caused the error
+ footer (Sequence[SupportsStr]): The footer that caused the error
expected_columns (int): The number of columns that were expected
"""
- def __init__(self, footer: list[SupportsStr], expected_columns: int):
+ def __init__(self, footer: Sequence[SupportsStr], expected_columns: int):
self.footer = footer
self.expected_columns = expected_columns
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Footer column count mismatch: {len(self.footer)} columns "
f"found, expected {self.expected_columns}."
@@ -62,12 +63,12 @@ class BodyColumnCountMismatchError(ColumnCountMismatchError):
This class is a subclass of :class:`ColumnCountMismatchError`.
Attributes:
- body (list[list[SupportsStr]]): The body that caused the error
+ body (Sequence[Sequence[SupportsStr]]): The body that caused the error
expected_columns (int): The number of columns that were expected
- first_invalid_row (list[SupportsStr]): The first row with an invalid column count
+ first_invalid_row (Sequence[SupportsStr]): The first row with an invalid column count
"""
- def __init__(self, body: list[list[SupportsStr]], expected_columns: int):
+ def __init__(self, body: Sequence[Sequence[SupportsStr]], expected_columns: int):
self.body = body
self.expected_columns = expected_columns
self.first_invalid_row = next(
@@ -75,7 +76,7 @@ def __init__(self, body: list[list[SupportsStr]], expected_columns: int):
)
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Body column count mismatch: A row with {len(self.first_invalid_row)} "
f"columns was found, expected {self.expected_columns}."
@@ -89,16 +90,16 @@ class AlignmentCountMismatchError(ColumnCountMismatchError):
This class is a subclass of :class:`ColumnCountMismatchError`.
Attributes:
- alignments (list[Alignment]): The alignments that caused the error
+ alignments (Sequence[Alignment]): The alignments that caused the error
expected_columns (int): The number of columns that were expected
"""
- def __init__(self, alignments: list[Alignment], expected_columns: int):
+ def __init__(self, alignments: Sequence[Alignment], expected_columns: int):
self.alignments = alignments
self.expected_columns = expected_columns
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Alignment count mismatch: {len(self.alignments)} alignments "
f"found, expected {self.expected_columns}."
@@ -112,22 +113,35 @@ class ColumnWidthsCountMismatchError(ColumnCountMismatchError):
This class is a subclass of :class:`ColumnCountMismatchError`.
Attributes:
- column_widths (list[Optional[int]]): The column widths that caused the error
+ column_widths (Sequence[Optional[int]]): The column widths that caused the error
expected_columns (int): The number of columns that were expected
"""
- def __init__(self, column_widths: list[int | None], expected_columns: int):
+ def __init__(self, column_widths: Sequence[int | None], expected_columns: int):
self.column_widths = column_widths
self.expected_columns = expected_columns
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Column widths count mismatch: {len(self.column_widths)} column widths "
f"found, expected {self.expected_columns}."
)
+class NoHeaderBodyOrFooterError(TableOptionError):
+ """Exception raised when no header, body or footer is provided
+
+ This class is a subclass of :class:`TableOptionError`.
+ """
+
+ def __init__(self):
+ super().__init__(self._message())
+
+ def _message(self) -> str:
+ return "At least one of header, body or footer must be provided."
+
+
class InvalidCellPaddingError(TableOptionError):
"""Exception raised when the cell padding is invalid
@@ -141,7 +155,7 @@ def __init__(self, padding: int):
self.padding = padding
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return f"Invalid cell padding: {self.padding} is not a positive integer."
@@ -163,7 +177,7 @@ def __init__(self, column_index: int, column_width: int, min_width: int):
self.min_width = min_width
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Column width too small: The column width for column index {self.column_index} "
f" of `column_widths` is {self.column_width}, but the minimum width "
@@ -184,7 +198,7 @@ def __init__(self, alignment: Any):
self.alignment = alignment
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Invalid alignment: {self.alignment!r} is not a valid alignment. "
f"Valid alignments are: {', '.join(a.__repr__() for a in Alignment)}"
@@ -208,7 +222,7 @@ def __init__(self, string: str, max_characters: int):
self.max_characters = max_characters
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Too many characters for table style: {len(self.string)} characters "
f"found, but the maximum number of characters allowed is {self.max_characters}."
@@ -234,7 +248,7 @@ def __init__(self, string: str, max_characters: int):
self.max_characters = max_characters
super().__init__(self._message())
- def _message(self):
+ def _message(self) -> str:
return (
f"Too few characters for table style: {len(self.string)} characters "
f"found, but table styles can accept {self.max_characters} characters. "
diff --git a/table2ascii/merge.py b/table2ascii/merge.py
index 3e8888b..05b0073 100644
--- a/table2ascii/merge.py
+++ b/table2ascii/merge.py
@@ -39,5 +39,5 @@ class Merge(Enum):
LEFT = 0
- def __str__(self):
+ def __str__(self) -> str:
return ""
diff --git a/table2ascii/options.py b/table2ascii/options.py
index 9f73a16..36f6ee0 100644
--- a/table2ascii/options.py
+++ b/table2ascii/options.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from collections.abc import Sequence
from dataclasses import dataclass
from .alignment import Alignment
@@ -17,8 +18,8 @@ class Options:
first_col_heading: bool
last_col_heading: bool
- column_widths: list[int | None] | None
- alignments: list[Alignment] | None
+ column_widths: Sequence[int | None] | None
+ alignments: Sequence[Alignment] | None
cell_padding: int
style: TableStyle
use_wcwidth: bool
diff --git a/table2ascii/table_style.py b/table2ascii/table_style.py
index 620eb77..4336a06 100644
--- a/table2ascii/table_style.py
+++ b/table2ascii/table_style.py
@@ -128,11 +128,11 @@ def from_string(cls, string: str) -> "TableStyle":
raise TableStyleTooLongError(string, num_params)
# if the string is too short, show a warning and pad it with spaces
elif len(string) < num_params:
- string += " " * (num_params - len(string))
warnings.warn(TableStyleTooShortWarning(string, num_params), stacklevel=2)
+ string += " " * (num_params - len(string))
return cls(*string)
- def set(self, **kwargs) -> "TableStyle":
+ def set(self, **kwargs: str) -> "TableStyle":
"""Set attributes of the TableStyle
Args:
diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py
index 835db3d..1b3a523 100644
--- a/table2ascii/table_to_ascii.py
+++ b/table2ascii/table_to_ascii.py
@@ -2,6 +2,7 @@
import textwrap
from math import ceil, floor
+from collections.abc import Sequence
from wcwidth import wcswidth
@@ -15,6 +16,7 @@
FooterColumnCountMismatchError,
InvalidAlignmentError,
InvalidCellPaddingError,
+ NoHeaderBodyOrFooterError,
)
from .merge import Merge
from .options import Options
@@ -27,9 +29,9 @@ class TableToAscii:
def __init__(
self,
- header: list[SupportsStr] | None,
- body: list[list[SupportsStr]] | None,
- footer: list[SupportsStr] | None,
+ header: Sequence[SupportsStr] | None,
+ body: Sequence[Sequence[SupportsStr]] | None,
+ footer: Sequence[SupportsStr] | None,
options: Options,
):
"""Validate arguments and initialize fields
@@ -41,9 +43,9 @@ def __init__(
options: The options for the table
"""
# initialize fields
- self.__header = header
- self.__body = body
- self.__footer = footer
+ self.__header = list(header) if header else None
+ self.__body = list([list(row) for row in body]) if body else None
+ self.__footer = list(footer) if footer else None
self.__style = options.style
self.__first_col_heading = options.first_col_heading
self.__last_col_heading = options.last_col_heading
@@ -60,6 +62,10 @@ def __init__(
if body and any(len(row) != self.__columns for row in body):
raise BodyColumnCountMismatchError(body, self.__columns)
+ # check that at least one of header, body, or footer is not None
+ if not header and not body and not footer:
+ raise NoHeaderBodyOrFooterError()
+
# calculate or use given column widths
self.__column_widths = self.__calculate_column_widths(options.column_widths)
@@ -103,7 +109,7 @@ def widest_line(value: SupportsStr) -> int:
text = str(value)
return max(self.__str_width(line) for line in text.splitlines()) if len(text) else 0
- def get_column_width(row: list[SupportsStr], column: int) -> int:
+ def get_column_width(row: Sequence[SupportsStr], column: int) -> int:
"""Get the width of a cell in a column"""
value = row[column]
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:
column_widths.append(max(header_size, body_size, footer_size) + self.__cell_padding * 2)
return column_widths
- def __calculate_column_widths(self, user_column_widths: list[int | None] | None) -> list[int]:
+ def __calculate_column_widths(
+ self, user_column_widths: Sequence[int | None] | None
+ ) -> list[int]:
"""Calculate the width of each column in the table based on the cell values and provided column widths.
Args:
@@ -187,7 +195,7 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
raise InvalidAlignmentError(alignment)
def __wrap_long_lines_in_merged_cells(
- self, row: list[SupportsStr], column_separator: str
+ self, row: Sequence[SupportsStr], column_separator: str
) -> list[SupportsStr]:
"""Wrap long lines in merged cells to the width of the merged cell
@@ -219,9 +227,9 @@ def __row_to_ascii(
heading_col_sep: str,
column_separator: str,
right_edge: str,
- filler: str | list[SupportsStr],
- previous_content_row: list[SupportsStr] | None = None,
- next_content_row: list[SupportsStr] | None = None,
+ filler: str | Sequence[SupportsStr],
+ previous_content_row: Sequence[SupportsStr] | None = None,
+ next_content_row: Sequence[SupportsStr] | None = None,
top_tee: str | None = None,
bottom_tee: str | None = None,
heading_col_top_tee: str | None = None,
@@ -266,9 +274,9 @@ def __line_in_row_to_ascii(
heading_col_sep: str,
column_separator: str,
right_edge: str,
- filler: str | list[SupportsStr],
- previous_content_row: list[SupportsStr] | None = None,
- next_content_row: list[SupportsStr] | None = None,
+ filler: str | Sequence[SupportsStr],
+ previous_content_row: Sequence[SupportsStr] | None = None,
+ next_content_row: Sequence[SupportsStr] | None = None,
top_tee: str | None = None,
bottom_tee: str | None = None,
heading_col_top_tee: str | None = None,
@@ -306,9 +314,9 @@ def __line_in_cell_column_to_ascii(
heading_col_sep: str,
column_separator: str,
right_edge: str,
- filler: str | list[SupportsStr],
- previous_content_row: list[SupportsStr] | None = None,
- next_content_row: list[SupportsStr] | None = None,
+ filler: str | Sequence[SupportsStr],
+ previous_content_row: Sequence[SupportsStr] | None = None,
+ next_content_row: Sequence[SupportsStr] | None = None,
top_tee: str | None = None,
bottom_tee: str | None = None,
heading_col_top_tee: str | None = None,
@@ -373,7 +381,7 @@ def __line_in_cell_column_to_ascii(
return output + sep
def __get_padded_cell_line_content(
- self, line_index: int, col_index: int, column_separator: str, filler: list[SupportsStr]
+ self, line_index: int, col_index: int, column_separator: str, filler: Sequence[SupportsStr]
) -> str:
# If this is a merge cell, merge with the previous column
if filler[col_index] is Merge.LEFT:
@@ -437,7 +445,7 @@ def __bottom_edge_to_ascii(self) -> str:
heading_col_bottom_tee=self.__style.heading_col_bottom_tee,
)
- def __content_row_to_ascii(self, row: list[SupportsStr]) -> str:
+ def __content_row_to_ascii(self, row: Sequence[SupportsStr]) -> str:
"""Assembles a row of cell values into a single line of the ascii table
Returns:
@@ -453,8 +461,8 @@ def __content_row_to_ascii(self, row: list[SupportsStr]) -> str:
def __heading_sep_to_ascii(
self,
- previous_content_row: list[SupportsStr] | None = None,
- next_content_row: list[SupportsStr] | None = None,
+ previous_content_row: Sequence[SupportsStr] | None = None,
+ next_content_row: Sequence[SupportsStr] | None = None,
) -> str:
"""Assembles the separator below the header or above footer of the ascii table
@@ -475,7 +483,7 @@ def __heading_sep_to_ascii(
heading_col_bottom_tee=self.__style.heading_col_heading_row_bottom_tee,
)
- def __body_to_ascii(self, body: list[list[SupportsStr]]) -> str:
+ def __body_to_ascii(self, body: Sequence[Sequence[SupportsStr]]) -> str:
"""Assembles the body of the ascii table
Returns:
@@ -551,14 +559,14 @@ def to_ascii(self) -> str:
def table2ascii(
- header: list[SupportsStr] | None = None,
- body: list[list[SupportsStr]] | None = None,
- footer: list[SupportsStr] | None = None,
+ header: Sequence[SupportsStr] | None = None,
+ body: Sequence[Sequence[SupportsStr]] | None = None,
+ footer: Sequence[SupportsStr] | None = None,
*,
first_col_heading: bool = False,
last_col_heading: bool = False,
- column_widths: list[int | None] | None = None,
- alignments: list[Alignment] | None = None,
+ column_widths: Sequence[int | None] | None = None,
+ alignments: Sequence[Alignment] | None = None,
cell_padding: int = 1,
style: TableStyle = PresetStyle.double_thin_compact,
use_wcwidth: bool = True,
@@ -581,8 +589,8 @@ def table2ascii(
Defaults to :py:obj:`False`.
column_widths: List of widths in characters for each column. Any value of :py:obj:`None`
indicates that the column width should be determined automatically. If :py:obj:`None`
- is passed instead of a :class:`list`, all columns will be automatically sized.
- Defaults to :py:obj:`None`.
+ is passed instead of a :class:`~collections.abc.Sequence`, all columns will be automatically
+ sized. Defaults to :py:obj:`None`.
alignments: List of alignments for each column
(ex. ``[Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT]``). If not specified or set to
:py:obj:`None`, all columns will be center-aligned. Defaults to :py:obj:`None`.
diff --git a/tests/test_convert.py b/tests/test_convert.py
index 60e696a..3030cd6 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -1,7 +1,11 @@
import pytest
from table2ascii import table2ascii as t2a
-from table2ascii.exceptions import BodyColumnCountMismatchError, FooterColumnCountMismatchError
+from table2ascii.exceptions import (
+ BodyColumnCountMismatchError,
+ FooterColumnCountMismatchError,
+ NoHeaderBodyOrFooterError,
+)
def test_header_body_footer():
@@ -117,6 +121,11 @@ def test_footer():
assert text == expected
+def test_no_header_body_or_footer():
+ with pytest.raises(NoHeaderBodyOrFooterError):
+ t2a()
+
+
def test_header_footer_unequal():
with pytest.raises(FooterColumnCountMismatchError):
t2a(