Skip to content

Commit

Permalink
Clean up Exception structures
Browse files Browse the repository at this point in the history
Most exceptions had a messages dict structure, that only made sense in
Validation exceptions. These series of commits change exception
structures to be simpler, and use messages dict structure only where necessary.
  • Loading branch information
subhashb committed Jul 27, 2024
1 parent 6206f27 commit eb3f13c
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/protean/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def server(
try:
domain = derive_domain(domain)
except NoDomainException as exc:
logger.error(f"Error loading Protean domain: {exc.messages}")
logger.error(f"Error loading Protean domain: {exc.args[0]}")
raise typer.Abort()

from protean.server import Engine
Expand Down
2 changes: 1 addition & 1 deletion src/protean/cli/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def shell(
try:
domain_instance = derive_domain(domain)
except NoDomainException as exc:
logger.error(f"Error loading Protean domain: {exc.messages}")
logger.error(f"Error loading Protean domain: {exc.args[0]}")
raise typer.Abort()

if traverse:
Expand Down
28 changes: 15 additions & 13 deletions src/protean/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
class ProteanException(Exception):
"""Base class for all Exceptions raised within Protean"""


class ProteanExceptionWithMessage(ProteanException):
def __init__(self, messages, traceback=None, **kwargs):
logger.debug(f"Exception:: {messages}")
self.messages = messages
Expand All @@ -20,56 +22,56 @@ def __str__(self):
return f"{dict(self.messages)}"

def __reduce__(self):
return (ProteanException, (self.messages,))
return (ProteanExceptionWithMessage, (self.messages,))


class NoDomainException(ProteanException):
"""Raised if a domain cannot be found or loaded in a module"""


class ConfigurationError(Exception):
class ConfigurationError(ProteanException):
"""Improper Configuration encountered like:
* An important configuration variable is missing
* Re-registration of Models
* Incorrect associations
"""


class ObjectNotFoundError(ProteanException):
class ObjectNotFoundError(ProteanExceptionWithMessage):
"""Object was not found, can raise 404"""


class TooManyObjectsError(Exception):
class TooManyObjectsError(ProteanException):
"""Expected one object, but found many"""


class InsufficientDataError(Exception):
class InsufficientDataError(ProteanException):
"""Object was not supplied with sufficient data"""


class InvalidDataError(ProteanException):
class InvalidDataError(ProteanExceptionWithMessage):
"""Data (type, value) is invalid"""


class InvalidStateError(Exception):
class InvalidStateError(ProteanException):
"""Object is in invalid state for the given operation
Equivalent to 409 (Conflict)"""


class InvalidOperationError(Exception):
class InvalidOperationError(ProteanException):
"""Operation being performed is not permitted"""


class NotSupportedError(Exception):
class NotSupportedError(ProteanException):
"""Object does not support the operation being performed"""


class IncorrectUsageError(ProteanException):
class IncorrectUsageError(ProteanExceptionWithMessage):
"""Usage of a Domain Element violates principles"""


class ValidationError(ProteanException):
class ValidationError(ProteanExceptionWithMessage):
"""Raised when validation fails on a field. Validators and custom fields should
raise this exception.
Expand All @@ -79,9 +81,9 @@ class ValidationError(ProteanException):
"""


class SendError(Exception):
class SendError(ProteanException):
"""Raised on email dispatch failure."""


class ExpectedVersionError(Exception):
class ExpectedVersionError(ProteanException):
"""Raised on expected version conflicts in EventSourcing"""
27 changes: 7 additions & 20 deletions src/protean/utils/domain_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ def find_domain_by_string(module, domain_name):
expr = ast.parse(domain_name.strip(), mode="eval").body
except SyntaxError:
raise NoDomainException(
{
"invalid": f"Failed to parse {domain_name!r} as an attribute name or function call."
}
f"Failed to parse {domain_name!r} as an attribute name or function call."
)

if isinstance(expr, ast.Name):
Expand All @@ -72,9 +70,7 @@ def find_domain_by_string(module, domain_name):
domain = getattr(module, name)
except AttributeError:
raise NoDomainException(
{
"invalid": f"Failed to find attribute {name!r} in {module.__name__!r}."
}
f"Failed to find attribute {name!r} in {module.__name__!r}."
)
elif isinstance(expr, ast.Call) and isinstance(expr.func, ast.Name):
# Handle function call, ensuring it's a simple function call without arguments
Expand All @@ -88,33 +84,24 @@ def find_domain_by_string(module, domain_name):
domain = domain_function() # Call the function to get the domain
else:
raise NoDomainException(
{
"invalid": f"{function_name!r} is not callable in {module.__name__!r}."
}
f"{function_name!r} is not callable in {module.__name__!r}."
)
except AttributeError:
raise NoDomainException(
{
"invalid": f"Failed to find function {function_name!r} in {module.__name__!r}."
}
f"Failed to find function {function_name!r} in {module.__name__!r}."
)
else:
raise NoDomainException(
{
"invalid": f"Function calls with arguments are not supported: {domain_name!r}."
}
f"Function calls with arguments are not supported: {domain_name!r}."
)
else:
raise NoDomainException(
{"invalid": f"Failed to parse {domain_name!r} as an attribute name."}
f"Failed to parse {domain_name!r} as an attribute name."
)

if not isinstance(domain, Domain):
raise NoDomainException(
{
"invalid": f"A valid Protean domain was not obtained from"
f" '{module.__name__}:{domain_name}'."
}
f"A valid Protean domain was not obtained from '{module.__name__}:{domain_name}'."
)

return domain
Expand Down
28 changes: 17 additions & 11 deletions src/protean/utils/reflection.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from typing import Any
from __future__ import annotations

from typing import TYPE_CHECKING, Type

from protean.exceptions import IncorrectUsageError

if TYPE_CHECKING:
from protean.fields.base import Field
from protean.utils.container import Element

_FIELDS = "__container_fields__"
_ID_FIELD_NAME = "__container_id_field_name__"


def fields(class_or_instance):
def fields(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
"""Return a tuple describing the fields of this dataclass.
Accepts a dataclass or an instance of one. Tuple elements are of
Expand All @@ -24,7 +30,7 @@ def fields(class_or_instance):
return fields_dict


def data_fields(class_or_instance):
def data_fields(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
"""Return a tuple describing the data fields of this dataclass.
Accepts a dataclass or an instance of one. Tuple elements are of
Expand All @@ -43,7 +49,7 @@ def data_fields(class_or_instance):
return fields_dict


def id_field(class_or_instance):
def id_field(class_or_instance: Type[Element] | Element) -> Field | None:
try:
field_name = getattr(class_or_instance, _ID_FIELD_NAME)
except AttributeError:
Expand All @@ -52,7 +58,7 @@ def id_field(class_or_instance):
return fields(class_or_instance)[field_name]


def has_id_field(class_or_instance: Any) -> bool:
def has_id_field(class_or_instance: Type[Element] | Element) -> bool:
"""Check if class/instance has an identity attribute.
Args:
Expand All @@ -64,12 +70,12 @@ def has_id_field(class_or_instance: Any) -> bool:
return hasattr(class_or_instance, _ID_FIELD_NAME)


def has_fields(class_or_instance):
def has_fields(class_or_instance: Type[Element] | Element) -> bool:
"""Check if Protean element encloses fields"""
return hasattr(class_or_instance, _FIELDS)


def attributes(class_or_instance):
def attributes(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
attributes_dict = {}

for _, field_obj in fields(class_or_instance).items():
Expand All @@ -92,7 +98,7 @@ def attributes(class_or_instance):
return attributes_dict


def unique_fields(class_or_instance):
def unique_fields(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
"""Return fields marked as unique for this class or instance"""
return {
field_name: field_obj
Expand All @@ -101,7 +107,7 @@ def unique_fields(class_or_instance):
}


def declared_fields(class_or_instance):
def declared_fields(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
"""Return a tuple describing the declared fields of this dataclass.
Accepts a dataclass or an instance of one. Tuple elements are of
Expand All @@ -126,7 +132,7 @@ def declared_fields(class_or_instance):
return fields_dict


def association_fields(class_or_instance):
def association_fields(class_or_instance: Type[Element] | Element) -> dict[str, Field]:
"""Return a tuple describing the association fields of this dataclass.
Accepts an Entity. Tuple elements are of type Field.
Expand All @@ -140,6 +146,6 @@ def association_fields(class_or_instance):
}


def has_association_fields(class_or_instance):
def has_association_fields(class_or_instance: Type[Element] | Element) -> bool:
"""Check if Protean element encloses association fields"""
return bool(association_fields(class_or_instance))

0 comments on commit eb3f13c

Please sign in to comment.