Skip to content

Commit 69f8f08

Browse files
refactor: Created custom exception classes (#74)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 736eb99 commit 69f8f08

File tree

7 files changed

+244
-21
lines changed

7 files changed

+244
-21
lines changed

docs/source/api.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,27 @@ TableStyle
2727
~~~~~~~~~~
2828

2929
.. autoclass:: TableStyle
30-
:members:
30+
:members:
31+
32+
Exceptions
33+
~~~~~~~~~~
34+
35+
.. autoexception:: table2ascii.exceptions.Table2AsciiError
36+
37+
.. autoexception:: table2ascii.exceptions.TableOptionError
38+
39+
.. autoexception:: table2ascii.exceptions.ColumnCountMismatchError
40+
41+
.. autoexception:: table2ascii.exceptions.FooterColumnCountMismatchError
42+
43+
.. autoexception:: table2ascii.exceptions.BodyColumnCountMismatchError
44+
45+
.. autoexception:: table2ascii.exceptions.AlignmentCountMismatchError
46+
47+
.. autoexception:: table2ascii.exceptions.InvalidCellPaddingError
48+
49+
.. autoexception:: table2ascii.exceptions.ColumnWidthsCountMismatchError
50+
51+
.. autoexception:: table2ascii.exceptions.ColumnWidthTooSmallError
52+
53+
.. autoexception:: table2ascii.exceptions.InvalidAlignmentError

table2ascii/exceptions.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from __future__ import annotations
2+
from typing import Any
3+
4+
from .alignment import Alignment
5+
6+
from .annotations import SupportsStr
7+
8+
9+
class Table2AsciiError(Exception):
10+
"""Base class for all table2ascii exceptions"""
11+
12+
def _message(self):
13+
"""Return the error message"""
14+
raise NotImplementedError
15+
16+
17+
class TableOptionError(Table2AsciiError, ValueError):
18+
"""Base class for exceptions raised when an invalid option
19+
is passed when creating an ascii table
20+
21+
This class is a subclass of :class:`Table2AsciiError` and :class:`ValueError`.
22+
"""
23+
24+
25+
class ColumnCountMismatchError(TableOptionError):
26+
"""Base class for exceptions raised when a parameter has an
27+
invalid number of columns
28+
29+
This class is a subclass of :class:`TableOptionError`.
30+
"""
31+
32+
expected_columns: int
33+
34+
35+
class FooterColumnCountMismatchError(ColumnCountMismatchError):
36+
"""Exception raised when the number of columns in the footer
37+
does not match the number of columns in the header
38+
39+
This class is a subclass of :class:`ColumnCountMismatchError`.
40+
41+
Attributes:
42+
footer (list[SupportsStr]): The footer that caused the error
43+
expected_columns (int): The number of columns that were expected
44+
"""
45+
46+
def __init__(self, footer: list[SupportsStr], expected_columns: int):
47+
self.footer = footer
48+
self.expected_columns = expected_columns
49+
super().__init__(self._message())
50+
51+
def _message(self):
52+
return (
53+
f"Footer column count mismatch: {len(self.footer)} columns "
54+
f"found, expected {self.expected_columns}."
55+
)
56+
57+
58+
class BodyColumnCountMismatchError(ColumnCountMismatchError):
59+
"""Exception raised when the number of columns in the body
60+
does not match the number of columns in the footer or header
61+
62+
This class is a subclass of :class:`ColumnCountMismatchError`.
63+
64+
Attributes:
65+
body (list[list[SupportsStr]]): The body that caused the error
66+
expected_columns (int): The number of columns that were expected
67+
first_invalid_row (list[SupportsStr]): The first row with an invalid column count
68+
"""
69+
70+
def __init__(self, body: list[list[SupportsStr]], expected_columns: int):
71+
self.body = body
72+
self.expected_columns = expected_columns
73+
self.first_invalid_row = next(
74+
(row for row in self.body if len(row) != self.expected_columns)
75+
)
76+
super().__init__(self._message())
77+
78+
def _message(self):
79+
return (
80+
f"Body column count mismatch: A row with {len(self.first_invalid_row)} "
81+
f"columns was found, expected {self.expected_columns}."
82+
)
83+
84+
85+
class AlignmentCountMismatchError(ColumnCountMismatchError):
86+
"""Exception raised when the number of alignments does not match
87+
the number of columns in the table
88+
89+
This class is a subclass of :class:`ColumnCountMismatchError`.
90+
91+
Attributes:
92+
alignments (list[Alignment]): The alignments that caused the error
93+
expected_columns (int): The number of columns that were expected
94+
"""
95+
96+
def __init__(self, alignments: list[Alignment], expected_columns: int):
97+
self.alignments = alignments
98+
self.expected_columns = expected_columns
99+
super().__init__(self._message())
100+
101+
def _message(self):
102+
return (
103+
f"Alignment count mismatch: {len(self.alignments)} alignments "
104+
f"found, expected {self.expected_columns}."
105+
)
106+
107+
108+
class ColumnWidthsCountMismatchError(ColumnCountMismatchError):
109+
"""Exception raised when the number of column widths does not match
110+
the number of columns in the table
111+
112+
This class is a subclass of :class:`ColumnCountMismatchError`.
113+
114+
Attributes:
115+
column_widths (list[Optional[int]]): The column widths that caused the error
116+
expected_columns (int): The number of columns that were expected
117+
"""
118+
119+
def __init__(self, column_widths: list[int | None], expected_columns: int):
120+
self.column_widths = column_widths
121+
self.expected_columns = expected_columns
122+
super().__init__(self._message())
123+
124+
def _message(self):
125+
return (
126+
f"Column widths count mismatch: {len(self.column_widths)} column widths "
127+
f"found, expected {self.expected_columns}."
128+
)
129+
130+
131+
class InvalidCellPaddingError(TableOptionError):
132+
"""Exception raised when the cell padding is invalid
133+
134+
This class is a subclass of :class:`TableOptionError`.
135+
136+
Attributes:
137+
padding (int): The padding that caused the error
138+
"""
139+
140+
def __init__(self, padding: int):
141+
self.padding = padding
142+
super().__init__(self._message())
143+
144+
def _message(self):
145+
return f"Invalid cell padding: {self.padding} is not a positive integer."
146+
147+
148+
class ColumnWidthTooSmallError(TableOptionError):
149+
"""Exception raised when the column width is smaller than the minimum
150+
number of characters that are required to display the content
151+
152+
This class is a subclass of :class:`TableOptionError`.
153+
154+
Attributes:
155+
column_index (int): The index of the column that caused the error
156+
column_width (int): The column width that caused the error
157+
min_width (int): The minimum width that is allowed
158+
"""
159+
160+
def __init__(self, column_index: int, column_width: int, min_width: int):
161+
self.column_index = column_index
162+
self.column_width = column_width
163+
self.min_width = min_width
164+
super().__init__(self._message())
165+
166+
def _message(self):
167+
return (
168+
f"Column width too small: The column width for column index {self.column_index} "
169+
f" of `column_widths` is {self.column_width}, but the minimum width "
170+
f"required to display the content is {self.min_width}."
171+
)
172+
173+
174+
class InvalidAlignmentError(TableOptionError):
175+
"""Exception raised when an invalid value is passed for an :class:`Alignment`
176+
177+
This class is a subclass of :class:`TableOptionError`.
178+
179+
Attributes:
180+
alignment (Any): The alignment value that caused the error
181+
"""
182+
183+
def __init__(self, alignment: Any):
184+
self.alignment = alignment
185+
super().__init__(self._message())
186+
187+
def _message(self):
188+
return (
189+
f"Invalid alignment: {self.alignment!r} is not a valid alignment. "
190+
f"Valid alignments are: {', '.join(a.__repr__() for a in Alignment)}"
191+
)

table2ascii/table_to_ascii.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
from .alignment import Alignment
66
from .annotations import SupportsStr
7+
from .exceptions import (
8+
AlignmentCountMismatchError,
9+
BodyColumnCountMismatchError,
10+
ColumnWidthTooSmallError,
11+
ColumnWidthsCountMismatchError,
12+
FooterColumnCountMismatchError,
13+
InvalidAlignmentError,
14+
InvalidCellPaddingError,
15+
)
716
from .options import Options
817
from .preset_style import PresetStyle
918
from .table_style import TableStyle
@@ -41,12 +50,10 @@ def __init__(
4150

4251
# check if footer has a different number of columns
4352
if footer and len(footer) != self.__columns:
44-
raise ValueError("Footer must have the same number of columns as the other rows")
53+
raise FooterColumnCountMismatchError(footer, self.__columns)
4554
# check if any rows in body have a different number of columns
4655
if body and any(len(row) != self.__columns for row in body):
47-
raise ValueError(
48-
"All rows in body must have the same number of columns as the other rows"
49-
)
56+
raise BodyColumnCountMismatchError(body, self.__columns)
5057

5158
# calculate or use given column widths
5259
self.__column_widths = self.__calculate_column_widths(options.column_widths)
@@ -55,11 +62,11 @@ def __init__(
5562

5663
# check if alignments specified have a different number of columns
5764
if options.alignments and len(options.alignments) != self.__columns:
58-
raise ValueError("Length of `alignments` list must equal the number of columns")
65+
raise AlignmentCountMismatchError(options.alignments, self.__columns)
5966

6067
# check if the cell padding is valid
6168
if self.__cell_padding < 0:
62-
raise ValueError("Cell padding must be greater than or equal to 0")
69+
raise InvalidCellPaddingError(self.__cell_padding)
6370

6471
def __count_columns(self) -> int:
6572
"""Get the number of columns in the table based on the provided header, footer, and body lists.
@@ -112,17 +119,15 @@ def __calculate_column_widths(self, user_column_widths: list[int | None] | None)
112119
if user_column_widths:
113120
# check that the right number of columns were specified
114121
if len(user_column_widths) != self.__columns:
115-
raise ValueError("Length of `column_widths` list must equal the number of columns")
122+
raise ColumnWidthsCountMismatchError(user_column_widths, self.__columns)
116123
# check that each column is at least as large as the minimum size
117124
for i in range(len(user_column_widths)):
118125
option = user_column_widths[i]
119126
minimum = column_widths[i]
120127
if option is None:
121128
option = minimum
122129
elif option < minimum:
123-
raise ValueError(
124-
f"The value at index {i} of `column_widths` is {option} which is less than the minimum {minimum}."
125-
)
130+
raise ColumnWidthTooSmallError(i, option, minimum)
126131
column_widths[i] = option
127132
return column_widths
128133

@@ -151,7 +156,7 @@ def __pad(self, cell_value: SupportsStr, width: int, alignment: Alignment) -> st
151156
if alignment == Alignment.RIGHT:
152157
# pad with spaces at the beginning
153158
return (" " * (width - len(padded_text))) + padded_text
154-
raise ValueError(f"The value '{alignment}' is not valid for alignment.")
159+
raise InvalidAlignmentError(alignment)
155160

156161
def __row_to_ascii(
157162
self,

tests/test_alignments.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from table2ascii import Alignment, table2ascii as t2a
4+
from table2ascii.exceptions import AlignmentCountMismatchError, InvalidAlignmentError
45

56

67
def test_first_left_four_right():
@@ -25,7 +26,7 @@ def test_first_left_four_right():
2526

2627

2728
def test_wrong_number_alignments():
28-
with pytest.raises(ValueError):
29+
with pytest.raises(AlignmentCountMismatchError):
2930
t2a(
3031
header=["#", "G", "H", "R", "S"],
3132
body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],
@@ -36,7 +37,7 @@ def test_wrong_number_alignments():
3637

3738

3839
def test_invalid_alignments():
39-
with pytest.raises(ValueError):
40+
with pytest.raises(InvalidAlignmentError):
4041
t2a(
4142
header=["#", "G", "H", "R", "S"],
4243
body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],

tests/test_cell_padding.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from table2ascii import Alignment, table2ascii as t2a
4+
from table2ascii.exceptions import InvalidCellPaddingError
45

56

67
def test_without_cell_padding():
@@ -72,7 +73,7 @@ def test_cell_padding_more_than_one():
7273

7374

7475
def test_negative_cell_padding():
75-
with pytest.raises(ValueError):
76+
with pytest.raises(InvalidCellPaddingError):
7677
t2a(
7778
header=["#", "G", "H", "R", "S"],
7879
body=[[1, 2, 3, 4, 5]],

tests/test_column_widths.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from table2ascii import table2ascii as t2a
4+
from table2ascii.exceptions import ColumnWidthsCountMismatchError, ColumnWidthTooSmallError
45

56

67
def test_column_widths():
@@ -70,7 +71,7 @@ def test_column_widths_contains_none():
7071

7172

7273
def test_wrong_number_column_widths():
73-
with pytest.raises(ValueError):
74+
with pytest.raises(ColumnWidthsCountMismatchError):
7475
t2a(
7576
header=["#", "G", "H", "R", "S"],
7677
body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],
@@ -82,7 +83,7 @@ def test_wrong_number_column_widths():
8283

8384

8485
def test_negative_column_widths():
85-
with pytest.raises(ValueError):
86+
with pytest.raises(ColumnWidthTooSmallError):
8687
t2a(
8788
header=["#", "G", "H", "R", "S"],
8889
body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],
@@ -94,7 +95,7 @@ def test_negative_column_widths():
9495

9596

9697
def test_column_width_less_than_size():
97-
with pytest.raises(ValueError):
98+
with pytest.raises(ColumnWidthTooSmallError):
9899
t2a(
99100
header=["Wide Column", "Another Wide Column", "H", "R", "S"],
100101
body=[["1", "30", "40", "35", "30"], ["2", "30", "40", "35", "30"]],

0 commit comments

Comments
 (0)