From eb3f13c889932cabd34e21a3ea20ed6f6c6b301d Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Fri, 26 Jul 2024 18:41:30 -0700 Subject: [PATCH] Clean up Exception structures 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. --- src/protean/cli/__init__.py | 2 +- src/protean/cli/shell.py | 2 +- src/protean/exceptions.py | 28 ++++++++++++++------------- src/protean/utils/domain_discovery.py | 27 +++++++------------------- src/protean/utils/reflection.py | 28 ++++++++++++++++----------- 5 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/protean/cli/__init__.py b/src/protean/cli/__init__.py index e4d2d2e4..8f2f1c35 100644 --- a/src/protean/cli/__init__.py +++ b/src/protean/cli/__init__.py @@ -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 diff --git a/src/protean/cli/shell.py b/src/protean/cli/shell.py index 20e6abb2..2c951991 100644 --- a/src/protean/cli/shell.py +++ b/src/protean/cli/shell.py @@ -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: diff --git a/src/protean/exceptions.py b/src/protean/exceptions.py index 3e887ad7..fbfe756d 100644 --- a/src/protean/exceptions.py +++ b/src/protean/exceptions.py @@ -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 @@ -20,14 +22,14 @@ 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 @@ -35,41 +37,41 @@ class ConfigurationError(Exception): """ -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. @@ -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""" diff --git a/src/protean/utils/domain_discovery.py b/src/protean/utils/domain_discovery.py index 5fe91d18..dbf42800 100644 --- a/src/protean/utils/domain_discovery.py +++ b/src/protean/utils/domain_discovery.py @@ -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): @@ -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 @@ -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 diff --git a/src/protean/utils/reflection.py b/src/protean/utils/reflection.py index 3c8038de..1d672b8c 100644 --- a/src/protean/utils/reflection.py +++ b/src/protean/utils/reflection.py @@ -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 @@ -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 @@ -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: @@ -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: @@ -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(): @@ -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 @@ -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 @@ -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. @@ -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))