Skip to content
Closed
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
8 changes: 7 additions & 1 deletion gemd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
EmpiricalFormula, NominalComposition, InChI, Smiles, \
LinkByUID, \
FileLink # noqa: F401
from .exceptions import GemdError, GemdValueError, GemdTypeError, \
GemdBoundsError, GemdValidationError, GemdEnumerationError, \
GemdSerializationError # noqa: F401

__all__ = ["Condition", "Parameter", "Property", "PropertyAndConditions",
"CategoricalBounds", "CompositionBounds", "IntegerBounds",
Expand All @@ -26,5 +29,8 @@
"UniformInteger", "DiscreteCategorical", "NominalCategorical",
"EmpiricalFormula", "NominalComposition", "InChI", "Smiles",
"LinkByUID",
"FileLink"
"FileLink",
"GemdError", "GemdValueError", "GemdTypeError",
"GemdBoundsError", "GemdValidationError",
"GemdEnumerationError", "GemdSerializationError",
]
2 changes: 1 addition & 1 deletion gemd/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.2.1"
__version__ = "2.2.2"
105 changes: 105 additions & 0 deletions gemd/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Custom exception hierarchy for gemd.

All exceptions inherit from both a gemd base class and the corresponding
Python built-in, so existing ``except ValueError`` code continues to work.
"""
from pint.errors import DimensionalityError, UndefinedUnitError

__all__ = [
"GemdError",
"GemdValueError",
"GemdBoundsError",
"GemdValidationError",
"GemdEnumerationError",
"GemdSerializationError",
"GemdTypeError",
"GemdUnitError",
"GemdIncompatibleUnitsError",
"GemdUndefinedUnitError",
"GemdKeyError",
]


class GemdError(Exception):
"""Base exception for all gemd errors."""

def __init__(self, message, *, context=None, guidance=None):
self.context = context
self.guidance = guidance
super().__init__(message)


class GemdValueError(GemdError, ValueError):
"""A value is invalid in the gemd context."""

def __init__(self, message, *, expected=None, received=None,
context=None, guidance=None):
self.expected = expected
self.received = received
super().__init__(message, context=context, guidance=guidance)


class GemdBoundsError(GemdValueError):
"""A value is outside the permitted bounds."""

def __init__(self, message, *, bounds=None, value=None,
context=None, guidance=None):
self.bounds = bounds
self.value = value
super().__init__(
message, expected=str(bounds), received=str(value),
context=context, guidance=guidance
)


class GemdValidationError(GemdValueError):
"""A value is inconsistent with its template or schema."""


class GemdEnumerationError(GemdValueError):
"""An invalid enumeration choice was provided."""


class GemdSerializationError(GemdValueError):
"""A serialization or deserialization error occurred."""


class GemdTypeError(GemdError, TypeError):
"""A wrong argument type was provided."""

def __init__(self, message, *, expected_type=None,
received_type=None, context=None, guidance=None):
self.expected_type = expected_type
self.received_type = received_type
super().__init__(message, context=context, guidance=guidance)


class GemdUnitError(GemdError):
"""Base for unit-related errors."""


class GemdIncompatibleUnitsError(GemdUnitError, DimensionalityError):
"""Units cannot be converted between each other."""

def __init__(self, message, *, context=None, guidance=None):
self.context = context
self.guidance = guidance
Exception.__init__(self, message)


class GemdUndefinedUnitError(GemdUnitError, UndefinedUnitError):
"""A unit string is not recognized."""

def __init__(self, message, *, context=None, guidance=None):
self.context = context
self.guidance = guidance
Exception.__init__(self, message)


class GemdKeyError(GemdError, KeyError):
"""A key conflict in a case-insensitive dictionary."""

def __init__(self, message, *, context=None, guidance=None):
self.context = context
self.guidance = guidance
Exception.__init__(self, message)
84 changes: 84 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for the custom exception hierarchy."""
import pytest

from gemd.exceptions import (
GemdError, GemdValueError, GemdBoundsError, GemdValidationError,
GemdEnumerationError, GemdSerializationError, GemdTypeError,
GemdUnitError, GemdIncompatibleUnitsError, GemdUndefinedUnitError,
GemdKeyError,
)


def test_hierarchy_isinstance():
"""Every leaf exception is an instance of GemdError."""
for cls in [GemdValueError, GemdBoundsError, GemdValidationError,
GemdEnumerationError, GemdSerializationError,
GemdTypeError, GemdUnitError,
GemdIncompatibleUnitsError, GemdUndefinedUnitError,
GemdKeyError]:
exc = cls("test")
assert isinstance(exc, GemdError)


def test_backward_compat_value_error():
"""Verify GemdValueError is caught by except ValueError."""
with pytest.raises(ValueError):
raise GemdValueError("bad value")


def test_backward_compat_type_error():
"""Verify GemdTypeError is caught by except TypeError."""
with pytest.raises(TypeError):
raise GemdTypeError("bad type")


def test_backward_compat_key_error():
"""Verify GemdKeyError is caught by except KeyError."""
with pytest.raises(KeyError):
raise GemdKeyError("bad key")


def test_bounds_error_is_value_error():
"""Verify GemdBoundsError is caught by except ValueError."""
with pytest.raises(ValueError):
raise GemdBoundsError("out of range")


def test_structured_attributes_value_error():
"""Verify GemdValueError stores structured data."""
exc = GemdValueError(
"bad", expected="int", received="str",
context="field x", guidance="pass an int"
)
assert exc.expected == "int"
assert exc.received == "str"
assert exc.context == "field x"
assert exc.guidance == "pass an int"
assert str(exc) == "bad"


def test_structured_attributes_type_error():
"""Verify GemdTypeError stores structured data."""
exc = GemdTypeError(
"wrong type", expected_type=int, received_type=str
)
assert exc.expected_type is int
assert exc.received_type is str


def test_structured_attributes_bounds_error():
"""Verify GemdBoundsError stores bounds and value."""
exc = GemdBoundsError(
"out of range", bounds="[0, 100]", value="200"
)
assert exc.bounds == "[0, 100]"
assert exc.value == "200"


def test_top_level_imports():
"""Verify exception classes are importable from the top-level package."""
from gemd import ( # noqa: F401
GemdError, GemdValueError, GemdTypeError,
GemdBoundsError, GemdValidationError,
GemdEnumerationError, GemdSerializationError,
)
Loading