diff --git a/docs/source/api.rst b/docs/source/api.rst index 5a46121..b890a84 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -27,4 +27,27 @@ TableStyle ~~~~~~~~~~ .. autoclass:: TableStyle - :members: \ No newline at end of file + :members: + +Exceptions +~~~~~~~~~~ + +.. autoexception:: table2ascii.exceptions.Table2AsciiError + +.. autoexception:: table2ascii.exceptions.TableOptionError + +.. autoexception:: table2ascii.exceptions.ColumnCountMismatchError + +.. autoexception:: table2ascii.exceptions.FooterColumnCountMismatchError + +.. autoexception:: table2ascii.exceptions.BodyColumnCountMismatchError + +.. autoexception:: table2ascii.exceptions.AlignmentCountMismatchError + +.. autoexception:: table2ascii.exceptions.InvalidCellPaddingError + +.. autoexception:: table2ascii.exceptions.ColumnWidthsCountMismatchError + +.. autoexception:: table2ascii.exceptions.ColumnWidthTooSmallError + +.. autoexception:: table2ascii.exceptions.InvalidAlignmentError diff --git a/table2ascii/alignment.py b/table2ascii/alignment.py index 68f48d5..56d062b 100644 --- a/table2ascii/alignment.py +++ b/table2ascii/alignment.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import IntEnum -class Alignment(Enum): +class Alignment(IntEnum): """Enum for text alignment types within a table cell Example:: diff --git a/table2ascii/exceptions.py b/table2ascii/exceptions.py new file mode 100644 index 0000000..efdf177 --- /dev/null +++ b/table2ascii/exceptions.py @@ -0,0 +1,191 @@ +from __future__ import annotations +from typing import Any + +from .alignment import Alignment + +from .annotations import SupportsStr + + +class Table2AsciiError(Exception): + """Base class for all table2ascii exceptions""" + + def _message(self): + """Return the error message""" + raise NotImplementedError + + +class TableOptionError(Table2AsciiError, ValueError): + """Base class for exceptions raised when an invalid option + is passed when creating an ascii table + + This class is a subclass of :class:`Table2AsciiError` and :class:`ValueError`. + """ + + +class ColumnCountMismatchError(TableOptionError): + """Base class for exceptions raised when a parameter has an + invalid number of columns + + This class is a subclass of :class:`TableOptionError`. + """ + + expected_columns: int + + +class FooterColumnCountMismatchError(ColumnCountMismatchError): + """Exception raised when the number of columns in the footer + does not match the number of columns in the header + + This class is a subclass of :class:`ColumnCountMismatchError`. + + Attributes: + footer (list[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): + self.footer = footer + self.expected_columns = expected_columns + super().__init__(self._message()) + + def _message(self): + return ( + f"Footer column count mismatch: {len(self.footer)} columns " + f"found, expected {self.expected_columns}." + ) + + +class BodyColumnCountMismatchError(ColumnCountMismatchError): + """Exception raised when the number of columns in the body + does not match the number of columns in the footer or header + + This class is a subclass of :class:`ColumnCountMismatchError`. + + Attributes: + body (list[list[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 + """ + + def __init__(self, body: list[list[SupportsStr]], expected_columns: int): + self.body = body + self.expected_columns = expected_columns + self.first_invalid_row = next( + (row for row in self.body if len(row) != self.expected_columns) + ) + super().__init__(self._message()) + + def _message(self): + return ( + f"Body column count mismatch: A row with {len(self.first_invalid_row)} " + f"columns was found, expected {self.expected_columns}." + ) + + +class AlignmentCountMismatchError(ColumnCountMismatchError): + """Exception raised when the number of alignments does not match + the number of columns in the table + + This class is a subclass of :class:`ColumnCountMismatchError`. + + Attributes: + alignments (list[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): + self.alignments = alignments + self.expected_columns = expected_columns + super().__init__(self._message()) + + def _message(self): + return ( + f"Alignment count mismatch: {len(self.alignments)} alignments " + f"found, expected {self.expected_columns}." + ) + + +class ColumnWidthsCountMismatchError(ColumnCountMismatchError): + """Exception raised when the number of column widths does not match + the number of columns in the table + + This class is a subclass of :class:`ColumnCountMismatchError`. + + Attributes: + column_widths (list[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): + self.column_widths = column_widths + self.expected_columns = expected_columns + super().__init__(self._message()) + + def _message(self): + return ( + f"Column widths count mismatch: {len(self.column_widths)} column widths " + f"found, expected {self.expected_columns}." + ) + + +class InvalidCellPaddingError(TableOptionError): + """Exception raised when the cell padding is invalid + + This class is a subclass of :class:`TableOptionError`. + + Attributes: + padding (int): The padding that caused the error + """ + + def __init__(self, padding: int): + self.padding = padding + super().__init__(self._message()) + + def _message(self): + return f"Invalid cell padding: {self.padding} is not a positive integer." + + +class ColumnWidthTooSmallError(TableOptionError): + """Exception raised when the column width is smaller than the minimum + number of characters that are required to display the content + + This class is a subclass of :class:`TableOptionError`. + + Attributes: + column_index (int): The index of the column that caused the error + column_width (int): The column width that caused the error + min_width (int): The minimum width that is allowed + """ + + def __init__(self, column_index: int, column_width: int, min_width: int): + self.column_index = column_index + self.column_width = column_width + self.min_width = min_width + super().__init__(self._message()) + + def _message(self): + 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 " + f"required to display the content is {self.min_width}." + ) + + +class InvalidAlignmentError(TableOptionError): + """Exception raised when an invalid value is passed for an :class:`Alignment` + + This class is a subclass of :class:`TableOptionError`. + + Attributes: + alignment (Any): The alignment value that caused the error + """ + + def __init__(self, alignment: Any): + self.alignment = alignment + super().__init__(self._message()) + + def _message(self): + return ( + f"Invalid alignment: {self.alignment!r} is not a valid alignment. " + f"Valid alignments are: {', '.join(a.__repr__() for a in Alignment)}" + ) diff --git a/table2ascii/table_to_ascii.py b/table2ascii/table_to_ascii.py index 70359c8..a8ff2c6 100644 --- a/table2ascii/table_to_ascii.py +++ b/table2ascii/table_to_ascii.py @@ -4,6 +4,15 @@ from .alignment import Alignment from .annotations import SupportsStr +from .exceptions import ( + AlignmentCountMismatchError, + BodyColumnCountMismatchError, + ColumnWidthTooSmallError, + ColumnWidthsCountMismatchError, + FooterColumnCountMismatchError, + InvalidAlignmentError, + InvalidCellPaddingError, +) from .options import Options from .preset_style import PresetStyle from .table_style import TableStyle @@ -41,12 +50,10 @@ def __init__( # check if footer has a different number of columns if footer and len(footer) != self.__columns: - raise ValueError("Footer must have the same number of columns as the other rows") + raise FooterColumnCountMismatchError(footer, self.__columns) # check if any rows in body have a different number of columns if body and any(len(row) != self.__columns for row in body): - raise ValueError( - "All rows in body must have the same number of columns as the other rows" - ) + raise BodyColumnCountMismatchError(body, self.__columns) # calculate or use given column widths self.__column_widths = self.__calculate_column_widths(options.column_widths) @@ -55,11 +62,11 @@ def __init__( # check if alignments specified have a different number of columns if options.alignments and len(options.alignments) != self.__columns: - raise ValueError("Length of `alignments` list must equal the number of columns") + raise AlignmentCountMismatchError(options.alignments, self.__columns) # check if the cell padding is valid if self.__cell_padding < 0: - raise ValueError("Cell padding must be greater than or equal to 0") + raise InvalidCellPaddingError(self.__cell_padding) def __count_columns(self) -> int: """Get the number of columns in the table based on the provided header, footer, and body lists. @@ -112,7 +119,7 @@ def __calculate_column_widths(self, user_column_widths: list[int | None] | None) if user_column_widths: # check that the right number of columns were specified if len(user_column_widths) != self.__columns: - raise ValueError("Length of `column_widths` list must equal the number of columns") + raise ColumnWidthsCountMismatchError(user_column_widths, self.__columns) # check that each column is at least as large as the minimum size for i in range(len(user_column_widths)): option = user_column_widths[i] @@ -120,9 +127,7 @@ def __calculate_column_widths(self, user_column_widths: list[int | None] | None) if option is None: option = minimum elif option < minimum: - raise ValueError( - f"The value at index {i} of `column_widths` is {option} which is less than the minimum {minimum}." - ) + raise ColumnWidthTooSmallError(i, option, minimum) column_widths[i] = option return column_widths @@ -151,7 +156,7 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st if alignment == Alignment.RIGHT: # pad with spaces at the beginning return (" " * (width - len(padded_text))) + padded_text - raise ValueError(f"The value '{alignment}' is not valid for alignment.") + raise InvalidAlignmentError(alignment) def __row_to_ascii( self, diff --git a/tests/test_alignments.py b/tests/test_alignments.py index b0c18b2..3e8b874 100644 --- a/tests/test_alignments.py +++ b/tests/test_alignments.py @@ -1,6 +1,7 @@ import pytest from table2ascii import Alignment, table2ascii as t2a +from table2ascii.exceptions import AlignmentCountMismatchError, InvalidAlignmentError def test_first_left_four_right(): @@ -25,7 +26,7 @@ def test_first_left_four_right(): def test_wrong_number_alignments(): - with pytest.raises(ValueError): + with pytest.raises(AlignmentCountMismatchError): t2a( header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], @@ -36,7 +37,7 @@ def test_wrong_number_alignments(): def test_invalid_alignments(): - with pytest.raises(ValueError): + with pytest.raises(InvalidAlignmentError): t2a( header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], diff --git a/tests/test_cell_padding.py b/tests/test_cell_padding.py index 6190053..7d68275 100644 --- a/tests/test_cell_padding.py +++ b/tests/test_cell_padding.py @@ -1,6 +1,7 @@ import pytest from table2ascii import Alignment, table2ascii as t2a +from table2ascii.exceptions import InvalidCellPaddingError def test_without_cell_padding(): @@ -72,7 +73,7 @@ def test_cell_padding_more_than_one(): def test_negative_cell_padding(): - with pytest.raises(ValueError): + with pytest.raises(InvalidCellPaddingError): t2a( header=["#", "G", "H", "R", "S"], body=[[1, 2, 3, 4, 5]], diff --git a/tests/test_column_widths.py b/tests/test_column_widths.py index 60f23f9..5a3f1ba 100644 --- a/tests/test_column_widths.py +++ b/tests/test_column_widths.py @@ -1,6 +1,7 @@ import pytest from table2ascii import table2ascii as t2a +from table2ascii.exceptions import ColumnWidthsCountMismatchError, ColumnWidthTooSmallError def test_column_widths(): @@ -70,7 +71,7 @@ def test_column_widths_contains_none(): def test_wrong_number_column_widths(): - with pytest.raises(ValueError): + with pytest.raises(ColumnWidthsCountMismatchError): t2a( header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], @@ -82,7 +83,7 @@ def test_wrong_number_column_widths(): def test_negative_column_widths(): - with pytest.raises(ValueError): + with pytest.raises(ColumnWidthTooSmallError): t2a( header=["#", "G", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], @@ -94,7 +95,7 @@ def test_negative_column_widths(): def test_column_width_less_than_size(): - with pytest.raises(ValueError): + with pytest.raises(ColumnWidthTooSmallError): t2a( header=["Wide Column", "Another Wide Column", "H", "R", "S"], body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]], diff --git a/tests/test_convert.py b/tests/test_convert.py index 3e7a045..414636f 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,6 +1,7 @@ import pytest from table2ascii import table2ascii as t2a +from table2ascii.exceptions import BodyColumnCountMismatchError, FooterColumnCountMismatchError def test_header_body_footer(): @@ -117,7 +118,7 @@ def test_footer(): def test_header_footer_unequal(): - with pytest.raises(ValueError): + with pytest.raises(FooterColumnCountMismatchError): t2a( header=["H", "R", "S"], footer=["SUM", "130", "140", "135", "130"], @@ -126,7 +127,7 @@ def test_header_footer_unequal(): def test_header_body_unequal(): - with pytest.raises(ValueError): + with pytest.raises(BodyColumnCountMismatchError): t2a( header=["#", "G", "H", "R", "S"], body=[ @@ -139,7 +140,7 @@ def test_header_body_unequal(): def test_footer_body_unequal(): - with pytest.raises(ValueError): + with pytest.raises(BodyColumnCountMismatchError): t2a( body=[ ["0", "45", "30", "32", "28"],