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( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy