Skip to content

Commit

Permalink
refactor: Implementation of custom exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
janezlapajne committed Sep 16, 2024
1 parent 9ffa706 commit e53e3fc
Show file tree
Hide file tree
Showing 27 changed files with 332 additions and 105 deletions.
85 changes: 85 additions & 0 deletions siapy/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Any


class SiapyError(Exception):
"""Base exception for SiaPy library."""

def __init__(self, message: str, name: str = "SiaPy") -> None:
self.message: str = message
self.name: str = name
super().__init__(self.message, self.name)


class InvalidFilepathError(SiapyError):
"""Exception raised when a required file is not found."""

def __init__(self, filename: str) -> None:
self.filename: str = filename
super().__init__(f"File not found: {filename}")


class InvalidInputError(SiapyError):
"""Exception raised for invalid input."""

def __init__(self, input_value: Any, message: str = "Invalid input") -> None:
self.input_value: Any = input_value
self.message: str = message
super().__init__(f"{message}: {input_value}")


class InvalidTypeError(SiapyError):
"""Exception raised for invalid type."""

def __init__(
self,
input_value: Any,
allowed_types: type | tuple[type, ...],
message: str = "Invalid type",
) -> None:
self.input_value: Any = input_value
self.input_type: Any = type(input_value)
self.allowed_types: type | tuple[type, ...] = allowed_types
self.message: str = message
super().__init__(
f"{message}: {input_value} (type: {self.input_type}). Allowed types: {allowed_types}"
)


class ProcessingError(SiapyError):
"""Exception raised for errors during processing."""

def __init__(self, message: str = "An error occurred during processing") -> None:
self.message: str = message
super().__init__(message)


class ConfigurationError(SiapyError):
"""Exception raised for configuration errors."""

def __init__(self, message: str = "Configuration error") -> None:
self.message: str = message
super().__init__(message)


class MethodNotImplementedError(SiapyError):
"""Exception raised for not implemented methods."""

def __init__(self, class_name: str, method_name: str) -> None:
self.class_name: str = class_name
self.method_name: str = method_name
super().__init__(
f"Method '{method_name}' not implemented in class '{class_name}'"
)


class DirectInitializationError(SiapyError):
"""Exception raised when a class method is required to create an instance."""

def __init__(self, class_: type) -> None:
from siapy.utils.general import get_classmethods

self.class_name: str = class_.__class__.__name__
self.class_methods: list[str] = get_classmethods(class_)
super().__init__(
f"Use any of the @classmethod to create a new instance of '{self.class_name}': {self.class_methods}"
)
21 changes: 17 additions & 4 deletions siapy/datasets/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import pandas as pd
from pydantic import BaseModel, ConfigDict

from siapy.core.exceptions import InvalidInputError

from .helpers import generate_classification_target, generate_regression_target


Expand Down Expand Up @@ -163,14 +165,25 @@ def target_from_dict(data: dict[str, Any] | None) -> Target | None:
elif data_keys.issubset(classification_keys):
return ClassificationTarget.from_dict(data)
else:
raise ValueError("Invalid target dict.")
raise InvalidInputError(data, "Invalid target dict.")

def _validate_lengths(self) -> None:
if not (len(self.pixels) == len(self.signals) == len(self.metadata)):
raise ValueError("Lengths of pixels, signals, and metadata must be equal")
raise InvalidInputError(
{
"pixels_length": len(self.pixels),
"signals_length": len(self.signals),
"metadata_length": len(self.metadata),
},
"Lengths of pixels, signals, and metadata must be equal",
)
if self.target is not None and len(self.target) != len(self):
raise ValueError(
"Target length must be equal to the length of the dataset."
raise InvalidInputError(
{
"target_length": len(self.target),
"dataset_length": len(self),
},
"Target length must be equal to the length of the dataset.",
)

def to_dict(self) -> dict[str, Any]:
Expand Down
10 changes: 7 additions & 3 deletions siapy/datasets/tabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pandas as pd
from pydantic import BaseModel, ConfigDict

from siapy.core.exceptions import InvalidInputError
from siapy.core.types import ImageContainerType
from siapy.datasets.schemas import TabularDatasetData
from siapy.entities import Signatures, SpectralImage, SpectralImageSet
Expand Down Expand Up @@ -112,7 +113,10 @@ def generate_dataset_data(self, mean_signatures=True) -> TabularDatasetData:

def _check_data_entities(self):
if not self.data_entities:
raise ValueError(
f"No data_entities! You need to process image set first."
f"by running {self.process_image_data.__name__}() function."
raise InvalidInputError(
{
"data_entities": self.data_entities,
"required_action": f"Run {self.process_image_data.__name__}() to process image set.",
},
"No data_entities! You need to process the image set first.",
)
55 changes: 48 additions & 7 deletions siapy/entities/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import spectral as sp
from PIL import Image, ImageOps

from siapy.core.exceptions import InvalidFilepathError, InvalidInputError

from .shapes import Shape
from .signatures import Signatures

Expand Down Expand Up @@ -101,12 +103,25 @@ def get_by_name(self, name: str) -> Shape | None:
def _check_shape_type(self, shapes: Shape | Iterable[Shape]):
if isinstance(shapes, Shape):
return

if not isinstance(shapes, Iterable):
raise ValueError(
"Shapes must be an instance of Shape or an iterable of Shape instances."
raise InvalidInputError(
{
"shapes_type": type(shapes).__name__,
},
"Shapes must be an instance of Shape or an iterable of Shape instances.",
)
if not all(isinstance(shape, Shape) for shape in shapes):
raise ValueError("All items must be instances of Shape subclass.")
raise InvalidInputError(
{
"invalid_items": [
type(shape).__name__
for shape in shapes
if not isinstance(shape, Shape)
],
},
"All items must be instances of Shape subclass.",
)


@dataclass
Expand Down Expand Up @@ -138,9 +153,16 @@ def __eq__(self, other: Any) -> bool:
def envi_open(
cls, *, header_path: str | Path, image_path: str | Path | None = None
) -> "SpectralImage":
if not Path(header_path).exists():
raise InvalidFilepathError(str(header_path))
sp_file = sp.envi.open(file=header_path, image=image_path)
if isinstance(sp_file, sp.io.envi.SpectralLibrary):
raise ValueError("Opened file of type SpectralLibrary")
raise InvalidInputError(
{
"file_type": type(sp_file).__name__,
},
"Opened file of type SpectralLibrary",
)
return cls(sp_file)

@property
Expand Down Expand Up @@ -268,9 +290,28 @@ def _parse():

try:
return _parse()

except ValueError as e:
raise ValueError(f"Error parsing description: {e}") from e
raise InvalidInputError(
{
"description": description,
"error": str(e),
},
f"Error parsing description: {e}",
) from e
except KeyError as e:
raise KeyError(f"Missing key in description: {e}") from e
raise InvalidInputError(
{
"description": description,
"error": str(e),
},
f"Missing key in description: {e}",
) from e
except Exception as e:
raise Exception(f"Unexpected error parsing description: {e}") from e
raise InvalidInputError(
{
"description": description,
"error": str(e),
},
f"Unexpected error parsing description: {e}",
) from e
9 changes: 8 additions & 1 deletion siapy/entities/imagesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rich.progress import track

from siapy.core import logger
from siapy.core.exceptions import InvalidInputError

from .images import SpectralImage

Expand Down Expand Up @@ -35,7 +36,13 @@ def from_paths(
image_paths: Sequence[str | Path] | None = None,
):
if image_paths is not None and len(header_paths) != len(image_paths):
raise ValueError("The length of hdr_paths and img_path must be equal.")
raise InvalidInputError(
{
"header_paths_length": len(header_paths),
"image_paths_length": len(image_paths),
},
"The length of hdr_paths and img_path must be equal.",
)

if image_paths is None:
spectral_images = [
Expand Down
13 changes: 9 additions & 4 deletions siapy/entities/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import numpy as np
from matplotlib.path import Path

from siapy.core.exceptions import InvalidInputError, MethodNotImplementedError

from .pixels import Pixels

SHAPE_TYPE_RECTANGLE = "rectangle"
Expand Down Expand Up @@ -44,7 +46,12 @@ def from_shape_type(
label=label,
)
else:
raise ValueError(f"Unsupported shape type: {shape_type}")
raise InvalidInputError(
{
"shape_type": shape_type,
},
f"Unsupported shape type: {shape_type}",
)

@property
def shape_type(self) -> str:
Expand All @@ -60,9 +67,7 @@ def label(self) -> str | None:

@abstractmethod
def convex_hull(self):
raise NotImplementedError(
"convex_hull() method is not implemented for the base Shape class."
)
raise MethodNotImplementedError(self.__class__.__name__, "convex_hull")


class Rectangle(Shape):
Expand Down
12 changes: 5 additions & 7 deletions siapy/entities/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import numpy as np
import pandas as pd

from siapy.utils.general import get_classmethods
from siapy.core.exceptions import DirectInitializationError, InvalidInputError

from .pixels import Pixels

Expand Down Expand Up @@ -61,9 +61,7 @@ class Signatures:
_signals: Signals

def __init__(self, *args: Any, **kwargs: Any):
raise RuntimeError(
f"Use any of the @classmethod to create a new instance: {get_classmethods(Signatures)}"
)
raise DirectInitializationError(Signatures)

@classmethod
def _create(cls, pixels: Pixels, signals: Signals) -> "Signatures":
Expand All @@ -85,9 +83,9 @@ def from_dataframe(cls, dataframe: pd.DataFrame) -> "Signatures":
if not all(
coord in dataframe.columns for coord in [Pixels.coords.U, Pixels.coords.V]
):
raise ValueError(
f"DataFrame must include columns for both '{Pixels.coords.U}'"
f" and '{Pixels.coords.V}' coordinates."
raise InvalidInputError(
dataframe.columns.tolist(),
f"DataFrame must include columns for both '{Pixels.coords.U}' and '{Pixels.coords.V}' coordinates.",
)
pixels = Pixels(dataframe[[Pixels.coords.U, Pixels.coords.V]])
signals = Signals(dataframe.drop(columns=[Pixels.coords.U, Pixels.coords.V]))
Expand Down
5 changes: 3 additions & 2 deletions siapy/features/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from autofeat import AutoFeatClassifier, AutoFeatRegressor # type: ignore
from sklearn.base import BaseEstimator, TransformerMixin

from siapy.core.exceptions import MethodNotImplementedError
from siapy.features.helpers import FeatureSelectorConfig, feature_selector_factory
from siapy.features.spectral_indices import compute_spectral_indices
from siapy.utils.general import set_random_seed
Expand Down Expand Up @@ -142,8 +143,8 @@ def transform(self, data: pd.DataFrame) -> pd.DataFrame:
columns_select_idx = list(self.selector[1].k_feature_idx_)
df_indices = df_indices.iloc[:, columns_select_idx]
else:
raise AttributeError(
"The attribute 'k_feature_idx_' does not exist in the selector."
raise MethodNotImplementedError(
self.selector[1].__class__.__name__, "k_feature_idx_"
)
if self.merge_with_original:
return pd.concat([data, df_indices], axis=1)
Expand Down
7 changes: 5 additions & 2 deletions siapy/features/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import RobustScaler

from siapy.core.exceptions import InvalidInputError


class FeatureSelectorConfig(BaseModel):
k_features: Annotated[
Expand Down Expand Up @@ -63,8 +65,9 @@ def feature_selector_factory(
algo = RidgeClassifier()
scoring = "f1_weighted"
else:
raise ValueError(
f"Invalid problem type: '{problem_type}', possible values are: 'regression' or 'classification'"
raise InvalidInputError(
problem_type,
"Invalid problem type, possible values are: 'regression' or 'classification'",
)
sfs = SequentialFeatureSelector(
estimator=algo,
Expand Down
Loading

0 comments on commit e53e3fc

Please sign in to comment.