Skip to content

refactor: Created custom exception classes #74

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
Dec 12, 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
25 changes: 24 additions & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,27 @@ TableStyle
~~~~~~~~~~

.. autoclass:: TableStyle
:members:
: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
4 changes: 2 additions & 2 deletions table2ascii/alignment.py
Original file line number Diff line number Diff line change
@@ -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::
Expand Down
191 changes: 191 additions & 0 deletions table2ascii/exceptions.py
Original file line number Diff line number Diff line change
@@ -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)}"
)
27 changes: 16 additions & 11 deletions table2ascii/table_to_ascii.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -112,17 +119,15 @@ 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]
minimum = column_widths[i]
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

Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions tests/test_alignments.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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"]],
Expand All @@ -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"]],
Expand Down
3 changes: 2 additions & 1 deletion tests/test_cell_padding.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from table2ascii import Alignment, table2ascii as t2a
from table2ascii.exceptions import InvalidCellPaddingError


def test_without_cell_padding():
Expand Down Expand Up @@ -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]],
Expand Down
7 changes: 4 additions & 3 deletions tests/test_column_widths.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from table2ascii import table2ascii as t2a
from table2ascii.exceptions import ColumnWidthsCountMismatchError, ColumnWidthTooSmallError


def test_column_widths():
Expand Down Expand Up @@ -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"]],
Expand All @@ -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"]],
Expand All @@ -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"]],
Expand Down
Loading