diff --git a/docs/guides/change-state/commands.md b/docs/guides/change-state/commands.md index 9dedc83d..d6844a9d 100644 --- a/docs/guides/change-state/commands.md +++ b/docs/guides/change-state/commands.md @@ -81,9 +81,6 @@ Out[3]: diff --git a/docs/guides/domain-definition/events.md b/docs/guides/domain-definition/events.md index eea1fb85..1dfceafa 100644 --- a/docs/guides/domain-definition/events.md +++ b/docs/guides/domain-definition/events.md @@ -195,9 +195,5 @@ In [2]: renamed = UserRenamed(user_id=user.id, name="John Doe Jr.") In [3]: renamed.name = "John Doe Sr." ... -IncorrectUsageError: { - '_message': [ - 'Event/Command Objects are immutable and cannot be modified once created' - ] -} +IncorrectUsageError: 'Event/Command Objects are immutable and cannot be modified once created' ``` diff --git a/docs/guides/domain-definition/value-objects.md b/docs/guides/domain-definition/value-objects.md index 552d7461..a67beebc 100644 --- a/docs/guides/domain-definition/value-objects.md +++ b/docs/guides/domain-definition/value-objects.md @@ -228,7 +228,7 @@ In [3]: class Balance(BaseValueObject): ...: currency = String(max_length=3, unique=True) ...: amount = Float() ... -IncorrectUsageError: {'_value_object': ["Value Objects cannot contain fields marked 'unique' (field 'currency')"]} +IncorrectUsageError: "Value Objects cannot contain fields marked 'unique' (field 'currency')" ``` Same case if you try to find a Value Object's `id_field`: @@ -238,7 +238,7 @@ In [4]: from protean.reflection import id_field In [5]: id_field(Balance) ... -IncorrectUsageError: {"identity": [" does not have identity fields"]} +IncorrectUsageError: " does not have identity fields" ``` ## Immutability @@ -250,5 +250,5 @@ In [1]: bal1 = Balance(currency='USD', amount=100.0) In [2]: bal1.currency = "CAD" ... -IncorrectUsageError: {'_value_object': ["Value Objects are immutable and cannot be modified once created"]} +IncorrectUsageError: "Value Objects are immutable and cannot be modified once created" ``` diff --git a/src/protean/adapters/repository/elasticsearch.py b/src/protean/adapters/repository/elasticsearch.py index 439d02f4..7f661b87 100644 --- a/src/protean/adapters/repository/elasticsearch.py +++ b/src/protean/adapters/repository/elasticsearch.py @@ -235,10 +235,8 @@ def _update(self, model_obj: Any): except NotFoundError as exc: logger.error(f"Database Record not found: {exc}") raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with identifier {identifier} " + f"does not exist." ) try: diff --git a/src/protean/adapters/repository/memory.py b/src/protean/adapters/repository/memory.py index c57819ae..b6ac5438 100644 --- a/src/protean/adapters/repository/memory.py +++ b/src/protean/adapters/repository/memory.py @@ -390,10 +390,8 @@ def _update(self, model_obj): # Check if object is present if identifier not in conn._db["data"][self.schema_name]: raise ObjectNotFoundError( - { - "_entity": f"`{self.__class__.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.__class__.__name__}` object with identifier {identifier} " + f"does not exist." ) conn._db["data"][self.schema_name][identifier] = model_obj @@ -434,10 +432,8 @@ def _delete(self, model_obj): # Check if object is present if identifier not in conn._db["data"][self.schema_name]: raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with identifier {identifier} " + f"does not exist." ) del conn._db["data"][self.schema_name][identifier] diff --git a/src/protean/adapters/repository/sqlalchemy.py b/src/protean/adapters/repository/sqlalchemy.py index ccdeffc8..cbf4b204 100644 --- a/src/protean/adapters/repository/sqlalchemy.py +++ b/src/protean/adapters/repository/sqlalchemy.py @@ -383,10 +383,8 @@ def _update(self, model_obj): conn.rollback() conn.close() raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with identifier {identifier} " + f"does not exist." ) # Sync DB Record with current changes. When the session is committed, changes are automatically synced @@ -447,10 +445,8 @@ def _delete(self, model_obj): conn.rollback() conn.close() raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with identifier {identifier} " + f"does not exist." ) try: 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/core/aggregate.py b/src/protean/core/aggregate.py index 0697095b..c027a7c8 100644 --- a/src/protean/core/aggregate.py +++ b/src/protean/core/aggregate.py @@ -257,11 +257,7 @@ def apply(fn): if len(typing.get_type_hints(fn)) > 2: raise IncorrectUsageError( - { - "_entity": [ - f"Handler method `{fn.__name__}` has incorrect number of arguments" - ] - } + f"Handler method `{fn.__name__}` has incorrect number of arguments" ) try: @@ -276,11 +272,7 @@ def apply(fn): ) except StopIteration: raise IncorrectUsageError( - { - "_entity": [ - f"Apply method `{fn.__name__}` should accept an argument annotated with the Event class" - ] - } + f"Apply method `{fn.__name__}` should accept an argument annotated with the Event class" ) @functools.wraps(fn) diff --git a/src/protean/core/command.py b/src/protean/core/command.py index a4919b95..1a3265a0 100644 --- a/src/protean/core/command.py +++ b/src/protean/core/command.py @@ -59,11 +59,7 @@ def command_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of and not element_cls.meta_.abstract: raise IncorrectUsageError( - { - "_command": [ - f"Command `{element_cls.__name__}` needs to be associated with an aggregate or a stream" - ] - } + f"Command `{element_cls.__name__}` needs to be associated with an aggregate or a stream" ) return element_cls diff --git a/src/protean/core/command_handler.py b/src/protean/core/command_handler.py index 94a588a9..49d7e236 100644 --- a/src/protean/core/command_handler.py +++ b/src/protean/core/command_handler.py @@ -28,11 +28,7 @@ def command_handler_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of: raise IncorrectUsageError( - { - "_entity": [ - f"Command Handler `{element_cls.__name__}` needs to be associated with an Aggregate" - ] - } + f"Command Handler `{element_cls.__name__}` needs to be associated with an Aggregate" ) return element_cls diff --git a/src/protean/core/domain_service.py b/src/protean/core/domain_service.py index b238ac1a..b9fbe9d7 100644 --- a/src/protean/core/domain_service.py +++ b/src/protean/core/domain_service.py @@ -114,11 +114,7 @@ def domain_service_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of or len(element_cls.meta_.part_of) < 2: raise IncorrectUsageError( - { - "_entity": [ - f"Domain Service `{element_cls.__name__}` needs to be associated with two or more Aggregates" - ] - } + f"Domain Service `{element_cls.__name__}` needs to be associated with two or more Aggregates" ) # Iterate through methods marked as `@invariant` and record them for later use diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py index 428a9268..2d0feae0 100644 --- a/src/protean/core/entity.py +++ b/src/protean/core/entity.py @@ -621,11 +621,7 @@ def entity_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of: raise IncorrectUsageError( - { - "_entity": [ - f"Entity `{element_cls.__name__}` needs to be associated with an Aggregate" - ] - } + f"Entity `{element_cls.__name__}` needs to be associated with an Aggregate" ) # Set up reference fields diff --git a/src/protean/core/event.py b/src/protean/core/event.py index 7396b05f..cedaeb37 100644 --- a/src/protean/core/event.py +++ b/src/protean/core/event.py @@ -58,11 +58,7 @@ def domain_event_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of and not element_cls.meta_.abstract: raise IncorrectUsageError( - { - "_event": [ - f"Event `{element_cls.__name__}` needs to be associated with an aggregate or a stream" - ] - } + f"Event `{element_cls.__name__}` needs to be associated with an aggregate or a stream" ) return element_cls diff --git a/src/protean/core/event_handler.py b/src/protean/core/event_handler.py index 44b2d0dc..a832d794 100644 --- a/src/protean/core/event_handler.py +++ b/src/protean/core/event_handler.py @@ -36,11 +36,7 @@ def event_handler_factory(element_cls, domain, **opts): if not (element_cls.meta_.part_of or element_cls.meta_.stream_category): raise IncorrectUsageError( - { - "_entity": [ - f"Event Handler `{element_cls.__name__}` needs to be associated with an aggregate or a stream" - ] - } + f"Event Handler `{element_cls.__name__}` needs to be associated with an aggregate or a stream" ) return element_cls diff --git a/src/protean/core/event_sourced_repository.py b/src/protean/core/event_sourced_repository.py index 66c1d9a2..33b23e6a 100644 --- a/src/protean/core/event_sourced_repository.py +++ b/src/protean/core/event_sourced_repository.py @@ -33,9 +33,7 @@ def __init__(self, domain) -> None: def add(self, aggregate: BaseAggregate) -> None: if aggregate is None: - raise IncorrectUsageError( - {"_entity": ["Aggregate object to persist is invalid"]} - ) + raise IncorrectUsageError("Aggregate object to persist is invalid") # Proceed only if aggregate has events if len(aggregate._events) > 0: @@ -91,10 +89,8 @@ def get(self, identifier: Identifier) -> BaseAggregate: if not aggregate: raise ObjectNotFoundError( - { - "_entity": f"`{self.meta_.part_of.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.meta_.part_of.__name__}` object with identifier {identifier} " + f"does not exist." ) aggregate._event_position = aggregate._version @@ -107,20 +103,12 @@ def event_sourced_repository_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of: raise IncorrectUsageError( - { - "_entity": [ - f"Repository `{element_cls.__name__}` should be associated with an Aggregate" - ] - } + f"Repository `{element_cls.__name__}` should be associated with an Aggregate" ) if not element_cls.meta_.part_of.meta_.is_event_sourced: raise IncorrectUsageError( - { - "_entity": [ - f"Repository `{element_cls.__name__}` can only be associated with an Event Sourced Aggregate" - ] - } + f"Repository `{element_cls.__name__}` can only be associated with an Event Sourced Aggregate" ) return element_cls diff --git a/src/protean/core/model.py b/src/protean/core/model.py index 4da9b289..301ee4f9 100644 --- a/src/protean/core/model.py +++ b/src/protean/core/model.py @@ -41,11 +41,7 @@ def model_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of: raise IncorrectUsageError( - { - "_entity": [ - f"Model `{element_cls.__name__}` should be associated with an Entity or Aggregate" - ] - } + f"Model `{element_cls.__name__}` should be associated with an Entity or Aggregate" ) return element_cls diff --git a/src/protean/core/repository.py b/src/protean/core/repository.py index b518119b..8fc7bb84 100644 --- a/src/protean/core/repository.py +++ b/src/protean/core/repository.py @@ -266,11 +266,7 @@ def repository_factory(element_cls, domain, **opts): if not element_cls.meta_.part_of: raise IncorrectUsageError( - { - "_entity": [ - f"Repository `{element_cls.__name__}` should be associated with an Aggregate" - ] - } + f"Repository `{element_cls.__name__}` should be associated with an Aggregate" ) # FIXME Uncomment @@ -284,11 +280,7 @@ def repository_factory(element_cls, domain, **opts): database.value for database in Database ]: raise IncorrectUsageError( - { - "_entity": [ - f"Repository `{element_cls.__name__}` should be associated with a valid Database" - ] - } + f"Repository `{element_cls.__name__}` should be associated with a valid Database" ) return element_cls diff --git a/src/protean/core/subscriber.py b/src/protean/core/subscriber.py index 074bd1ef..b1b092ea 100644 --- a/src/protean/core/subscriber.py +++ b/src/protean/core/subscriber.py @@ -39,20 +39,12 @@ def subscriber_factory(element_cls, domain, **opts): if not element_cls.meta_.event: raise IncorrectUsageError( - { - "_entity": [ - f"Subscriber `{element_cls.__name__}` needs to be associated with an Event" - ] - } + f"Subscriber `{element_cls.__name__}` needs to be associated with an Event" ) if not element_cls.meta_.broker: raise IncorrectUsageError( - { - "_entity": [ - f"Subscriber `{element_cls.__name__}` needs to be associated with a Broker" - ] - } + f"Subscriber `{element_cls.__name__}` needs to be associated with a Broker" ) return element_cls diff --git a/src/protean/core/value_object.py b/src/protean/core/value_object.py index 8498522a..1b3ca9b9 100644 --- a/src/protean/core/value_object.py +++ b/src/protean/core/value_object.py @@ -44,12 +44,8 @@ def __validate_for_basic_field_types(subclass): # Value objects can hold all kinds of fields, except associations if isinstance(field_obj, (Reference, Association)): raise IncorrectUsageError( - { - "_value_object": [ - f"Value Objects cannot have associations. " - f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" - ] - } + f"Value Objects cannot have associations. " + f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" ) @classmethod @@ -57,11 +53,7 @@ def __validate_for_non_identifier_fields(subclass): for field_name, field_obj in fields(subclass).items(): if field_obj.identifier: raise IncorrectUsageError( - { - "_value_object": [ - f"Value Objects cannot contain fields marked 'identifier' (field '{field_name}')" - ] - } + f"Value Objects cannot contain fields marked 'identifier' (field '{field_name}')" ) @classmethod @@ -69,11 +61,7 @@ def __validate_for_non_unique_fields(subclass): for field_name, field_obj in fields(subclass).items(): if field_obj.unique: raise IncorrectUsageError( - { - "_value_object": [ - f"Value Objects cannot contain fields marked 'unique' (field '{field_name}')" - ] - } + f"Value Objects cannot contain fields marked 'unique' (field '{field_name}')" ) def __init__(self, *template, **kwargs): # noqa: C901 @@ -176,11 +164,7 @@ def __setattr__(self, name, value): return super().__setattr__(name, value) else: raise IncorrectUsageError( - { - "_value_object": [ - "Value Objects are immutable and cannot be modified once created" - ] - } + "Value Objects are immutable and cannot be modified once created" ) def _postcheck(self, return_errors=False): diff --git a/src/protean/core/view.py b/src/protean/core/view.py index 051197e0..e833d335 100644 --- a/src/protean/core/view.py +++ b/src/protean/core/view.py @@ -65,12 +65,8 @@ def __validate_for_basic_field_types(subclass): for field_name, field_obj in declared_fields(subclass).items(): if isinstance(field_obj, (Reference, Association, ValueObject)): raise IncorrectUsageError( - { - "_entity": [ - f"Views can only contain basic field types. " - f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" - ] - } + f"Views can only contain basic field types. " + f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" ) def __init__(self, *template, **kwargs): @@ -110,11 +106,7 @@ def view_factory(element_cls, domain, **opts): if not element_cls.meta_.abstract and not hasattr(element_cls, _ID_FIELD_NAME): raise IncorrectUsageError( - { - "_entity": [ - f"View `{element_cls.__name__}` needs to have at least one identifier" - ] - } + f"View `{element_cls.__name__}` needs to have at least one identifier" ) element_cls.meta_.provider = ( diff --git a/src/protean/domain/__init__.py b/src/protean/domain/__init__.py index b40d84ac..2fb4ca3d 100644 --- a/src/protean/domain/__init__.py +++ b/src/protean/domain/__init__.py @@ -431,7 +431,7 @@ def factory_for(self, domain_object_type): if domain_object_type.value not in factories: raise IncorrectUsageError( - {"_entity": [f"Unknown Element Type `{domain_object_type.value}`"]} + f"Unknown Element Type `{domain_object_type.value}`" ) return factories[domain_object_type.value] @@ -693,21 +693,13 @@ def _validate_domain(self): if isinstance(field_obj, (HasOne, HasMany)): if isinstance(field_obj.to_cls, str): raise IncorrectUsageError( - { - "element": ( - f"Unresolved target `{field_obj.to_cls}` for field " - f"`{aggregate.__name__}:{field_obj.name}`" - ) - } + f"Unresolved target `{field_obj.to_cls}` for field " + f"`{aggregate.__name__}:{field_obj.name}`" ) if field_obj.to_cls.element_type != DomainObjects.ENTITY: raise IncorrectUsageError( - { - "element": ( - f"Field `{field_obj.field_name}` in `{aggregate.cls.__name__}` " - "is not linked to an Entity class" - ) - } + f"Field `{field_obj.field_name}` in `{aggregate.cls.__name__}` " + "is not linked to an Entity class" ) # Check that no two event sourced aggregates have the same event class in their @@ -733,12 +725,8 @@ def _validate_domain(self): ) if len(duplicate_event_class_names) != 0: raise IncorrectUsageError( - { - "_event": [ - f"Events are associated with multiple event sourced aggregates: " - f"{', '.join(duplicate_event_class_names)}" - ] - } + f"Events are associated with multiple event sourced aggregates: " + f"{', '.join(duplicate_event_class_names)}" ) # Check that entities have the same provider as the aggregate @@ -748,12 +736,8 @@ def _validate_domain(self): != entity.cls.meta_.provider ): raise IncorrectUsageError( - { - "element": ( - f"Entity `{entity.cls.__name__}` has a different provider " - f"than its aggregate `{entity.cls.meta_.aggregate_cluster.__name__}`" - ) - } + f"Entity `{entity.cls.__name__}` has a different provider " + f"than its aggregate `{entity.cls.meta_.aggregate_cluster.__name__}`" ) def _assign_aggregate_clusters(self): @@ -822,9 +806,7 @@ def register_external_event(self, event_cls: Type[BaseEvent], type_string: str): not issubclass(event_cls, BaseEvent) or event_cls.element_type != DomainObjects.EVENT ): - raise IncorrectUsageError( - {"element": [f"Class `{event_cls.__name__}` is not an Event"]} - ) + raise IncorrectUsageError(f"Class `{event_cls.__name__}` is not an Event") self._events_and_commands[type_string] = event_cls @@ -845,23 +827,15 @@ def _setup_command_handlers(self): method._target_cls ) or not issubclass(method._target_cls, BaseCommand): raise IncorrectUsageError( - { - "_command_handler": [ - f"Method `{method_name}` in Command Handler `{element.cls.__name__}` " - "is not associated with a command" - ] - } + f"Method `{method_name}` in Command Handler `{element.cls.__name__}` " + "is not associated with a command" ) # Throw error if target_cls is not associated with an aggregate if not method._target_cls.meta_.part_of: raise IncorrectUsageError( - { - "_command_handler": [ - f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` " - "is not associated with an aggregate" - ] - } + f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` " + "is not associated with an aggregate" ) if ( @@ -869,12 +843,8 @@ def _setup_command_handlers(self): != element.cls.meta_.part_of ): raise IncorrectUsageError( - { - "_command_handler": [ - f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` " - "is not associated with the same aggregate as the Command Handler" - ] - } + f"Command `{method._target_cls.__name__}` in Command Handler `{element.cls.__name__}` " + "is not associated with the same aggregate as the Command Handler" ) command_type = ( @@ -1101,11 +1071,7 @@ def process(self, command: BaseCommand, asynchronous: bool = True) -> Optional[A not in self.registry._elements[DomainObjects.COMMAND.value] ): raise IncorrectUsageError( - { - "element": [ - f"Element {command.__class__.__name__} is not registered in domain {self.name}" - ] - } + f"Element {command.__class__.__name__} is not registered in domain {self.name}" ) command_with_metadata = self._enrich_command(command) @@ -1154,11 +1120,7 @@ def handlers_for(self, event: BaseEvent) -> List[BaseEventHandler]: def repository_for(self, element_cls) -> BaseRepository: if isinstance(element_cls, str): raise IncorrectUsageError( - { - "element": [ - f"Element {element_cls} is not registered in domain {self.name}" - ] - } + f"Element {element_cls} is not registered in domain {self.name}" ) if ( diff --git a/src/protean/exceptions.py b/src/protean/exceptions.py index 3e887ad7..1161e67f 100644 --- a/src/protean/exceptions.py +++ b/src/protean/exceptions.py @@ -3,6 +3,7 @@ """ import logging +from typing import Any, Optional logger = logging.getLogger(__name__) @@ -10,24 +11,38 @@ class ProteanException(Exception): """Base class for all Exceptions raised within Protean""" - def __init__(self, messages, traceback=None, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args) + + self.extra_info = kwargs.get("extra_info", None) + + def __reduce__(self) -> tuple[Any, tuple[Any]]: + return (self.__class__, (self.args[0],)) + + +class ProteanExceptionWithMessage(ProteanException): + def __init__( + self, messages: dict[str, str], traceback: Optional[str] = None, **kwargs: Any + ) -> None: logger.debug(f"Exception:: {messages}") + self.messages = messages self.traceback = traceback + super().__init__(**kwargs) - def __str__(self): + def __str__(self) -> str: return f"{dict(self.messages)}" - def __reduce__(self): - return (ProteanException, (self.messages,)) + def __reduce__(self) -> tuple[Any, tuple[Any]]: + 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 @@ -39,29 +54,29 @@ class ObjectNotFoundError(ProteanException): """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""" @@ -69,7 +84,7 @@ class IncorrectUsageError(ProteanException): """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 +94,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/fields/association.py b/src/protean/fields/association.py index 27ad2247..dc9d5d82 100644 --- a/src/protean/fields/association.py +++ b/src/protean/fields/association.py @@ -722,16 +722,12 @@ def get(self, instance, **kwargs): if len(data) == 0: raise exceptions.ObjectNotFoundError( - {"self.field_name": ["No linked entities matching criteria found"]} + "No linked entities matching criteria found" ) if len(data) > 1: raise exceptions.TooManyObjectsError( - { - "self.field_name": [ - "Multiple linked entities matching criteria found" - ] - } + "Multiple linked entities matching criteria found" ) return data[0] diff --git a/src/protean/fields/embedded.py b/src/protean/fields/embedded.py index e7524cf5..3091d660 100644 --- a/src/protean/fields/embedded.py +++ b/src/protean/fields/embedded.py @@ -73,14 +73,8 @@ def _validate_value_object_cls(self, value_object_cls): if not issubclass(value_object_cls, BaseValueObject): raise IncorrectUsageError( - { - "_value_object": [ - ( - f"`{value_object_cls.__name__}` is not a valid Value Object " - "and cannot be embedded in a Value Object field" - ) - ] - } + f"`{value_object_cls.__name__}` is not a valid Value Object " + "and cannot be embedded in a Value Object field" ) @property diff --git a/src/protean/port/dao.py b/src/protean/port/dao.py index c69ce2c1..c20dca61 100644 --- a/src/protean/port/dao.py +++ b/src/protean/port/dao.py @@ -234,10 +234,8 @@ def get(self, identifier: Any) -> BaseEntity: results = self.query.filter(**filters).all() if not results: raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with identifier {identifier} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with identifier {identifier} " + f"does not exist." ) if len(results) > 1: @@ -269,10 +267,8 @@ def find_by(self, **kwargs) -> "BaseEntity": if not results: raise ObjectNotFoundError( - { - "_entity": f"`{self.entity_cls.__name__}` object with values {[item for item in kwargs.items()]} " - f"does not exist." - } + f"`{self.entity_cls.__name__}` object with values {[item for item in kwargs.items()]} " + f"does not exist." ) if len(results) > 1: 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/eventing.py b/src/protean/utils/eventing.py index 94caa658..4ac0b800 100644 --- a/src/protean/utils/eventing.py +++ b/src/protean/utils/eventing.py @@ -83,12 +83,8 @@ def __validate_for_basic_field_types(subclass): # Value objects can hold all kinds of fields, except associations if isinstance(field_obj, (Reference, Association)): raise IncorrectUsageError( - { - "_message": [ - f"Events/Commands cannot have associations. " - f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" - ] - } + f"Events/Commands cannot have associations. " + f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" ) def __setattr__(self, name, value): @@ -96,11 +92,7 @@ def __setattr__(self, name, value): return super().__setattr__(name, value) else: raise IncorrectUsageError( - { - "_message": [ - "Event/Command Objects are immutable and cannot be modified once created" - ] - } + "Event/Command Objects are immutable and cannot be modified once created" ) @classmethod diff --git a/src/protean/utils/globals.py b/src/protean/utils/globals.py index e16d21e4..b5b55bd2 100644 --- a/src/protean/utils/globals.py +++ b/src/protean/utils/globals.py @@ -18,7 +18,7 @@ """ -def _lookup_domain_object(name) -> Any: +def _lookup_domain_object(name) -> Any | None: top = _domain_context_stack.top if top is None: warnings.warn( @@ -29,7 +29,7 @@ def _lookup_domain_object(name) -> Any: return getattr(top, name) -def _find_domain() -> Domain: +def _find_domain() -> Domain | None: top = _domain_context_stack.top if top is None: warnings.warn( diff --git a/src/protean/utils/mixins.py b/src/protean/utils/mixins.py index 285ace78..60647389 100644 --- a/src/protean/utils/mixins.py +++ b/src/protean/utils/mixins.py @@ -88,7 +88,7 @@ def to_object(self) -> Union[BaseEvent, BaseCommand]: ]: # We are dealing with a malformed or unknown message raise InvalidDataError( - {"_message": ["Message type is not supported for deserialization"]} + {"kind": ["Message type is not supported for deserialization"]} ) element_cls = current_domain._events_and_commands.get(self.metadata.type, None) diff --git a/src/protean/utils/reflection.py b/src/protean/utils/reflection.py index 3c8038de..59d4e927 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 @@ -17,14 +23,12 @@ def fields(class_or_instance): try: fields_dict = getattr(class_or_instance, _FIELDS) except AttributeError: - raise IncorrectUsageError( - {"field": [f"{class_or_instance} does not have fields"]} - ) + raise IncorrectUsageError(f"{class_or_instance} does not have fields") 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 @@ -36,14 +40,12 @@ def data_fields(class_or_instance): # Remove internal fields fields_dict.pop("_metadata", None) except AttributeError: - raise IncorrectUsageError( - {"field": [f"{class_or_instance} does not have fields"]} - ) + raise IncorrectUsageError(f"{class_or_instance} does not have fields") 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 +54,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 +66,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 +94,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 +103,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 @@ -119,14 +121,12 @@ def declared_fields(class_or_instance): fields_dict.pop("_version", None) fields_dict.pop("_metadata", None) except AttributeError: - raise IncorrectUsageError( - {"field": [f"{class_or_instance} does not have fields"]} - ) + raise IncorrectUsageError(f"{class_or_instance} does not have fields") 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 +140,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)) diff --git a/tests/command/test_command_field_types.py b/tests/command/test_command_field_types.py index e279e5d5..7f3d1162 100644 --- a/tests/command/test_command_field_types.py +++ b/tests/command/test_command_field_types.py @@ -33,8 +33,7 @@ class Register(BaseCommand): name = String() account = HasOne(Account) - assert exc.value.messages == { - "_message": [ - "Events/Commands cannot have associations. Remove account (HasOne) from class Register" - ] - } + assert ( + exc.value.args[0] + == "Events/Commands cannot have associations. Remove account (HasOne) from class Register" + ) diff --git a/tests/command/test_command_meta.py b/tests/command/test_command_meta.py index b3a2a1dc..945fc740 100644 --- a/tests/command/test_command_meta.py +++ b/tests/command/test_command_meta.py @@ -27,11 +27,10 @@ def test_command_definition_without_aggregate_or_stream(test_domain): with pytest.raises(IncorrectUsageError) as exc: test_domain.register(Register) - assert exc.value.messages == { - "_command": [ - "Command `Register` needs to be associated with an aggregate or a stream" - ] - } + assert ( + exc.value.args[0] + == "Command `Register` needs to be associated with an aggregate or a stream" + ) def test_that_abstract_commands_can_be_defined_without_aggregate_or_stream(test_domain): diff --git a/tests/command_handler/test_basics.py b/tests/command_handler/test_basics.py index ccdfc2ed..9b460521 100644 --- a/tests/command_handler/test_basics.py +++ b/tests/command_handler/test_basics.py @@ -43,11 +43,9 @@ def something(self, _: Registered): with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) - assert exc.value.messages == { - "_command_handler": [ - "Method `something` in Command Handler `UserCommandHandlers` is not associated with a command" - ] - } + assert exc.value.args[0] == ( + "Method `something` in Command Handler `UserCommandHandlers` is not associated with a command" + ) def test_commands_have_to_be_registered_with_an_aggregate(test_domain): @@ -62,11 +60,9 @@ def something(self, _: Register): with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) - assert exc.value.messages == { - "_command_handler": [ - "Command `Register` in Command Handler `UserCommandHandlers` is not associated with an aggregate" - ] - } + assert exc.value.args[0] == ( + "Command `Register` in Command Handler `UserCommandHandlers` is not associated with an aggregate" + ) def test_command_and_command_handler_have_to_be_associated_with_same_aggregate( @@ -89,10 +85,9 @@ class User2(BaseAggregate): with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) - assert exc.value.messages == { - "_command_handler": [ - "Command `Register` in Command Handler `UserCommandHandlers` is not associated with the same aggregate as the Command Handler" - ] - } + assert exc.value.args[0] == ( + "Command `Register` in Command Handler `UserCommandHandlers` is not " + "associated with the same aggregate as the Command Handler" + ) test_domain.register(UserCommandHandlers, part_of=User) diff --git a/tests/command_handler/test_command_handler_options.py b/tests/command_handler/test_command_handler_options.py index cb1a6f3a..fa09e157 100644 --- a/tests/command_handler/test_command_handler_options.py +++ b/tests/command_handler/test_command_handler_options.py @@ -24,11 +24,10 @@ class UserCommandHandlers(BaseCommandHandler): with pytest.raises(IncorrectUsageError) as exc: test_domain.register(UserCommandHandlers) - assert exc.value.messages == { - "_entity": [ - "Command Handler `UserCommandHandlers` needs to be associated with an Aggregate" - ] - } + assert ( + exc.value.args[0] + == "Command Handler `UserCommandHandlers` needs to be associated with an Aggregate" + ) def test_part_of_specified_as_a_meta_attribute(test_domain): diff --git a/tests/entity/test_entity_provider_option.py b/tests/entity/test_entity_provider_option.py index 67b287d7..122b6652 100644 --- a/tests/entity/test_entity_provider_option.py +++ b/tests/entity/test_entity_provider_option.py @@ -52,6 +52,7 @@ def test_entity_provider_is_same_as_aggregate_provider(self, test_domain): with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) - assert exc.value.messages == { - "element": "Entity `Dean` has a different provider than its aggregate `Department`" - } + assert ( + exc.value.args[0] + == "Entity `Dean` has a different provider than its aggregate `Department`" + ) diff --git a/tests/event/test_event_field_types.py b/tests/event/test_event_field_types.py index d9d631cf..0a0814e2 100644 --- a/tests/event/test_event_field_types.py +++ b/tests/event/test_event_field_types.py @@ -33,8 +33,6 @@ class UserRegistered(BaseEvent): name = String() account = HasOne(Account) - assert exc.value.messages == { - "_message": [ - "Events/Commands cannot have associations. Remove account (HasOne) from class UserRegistered" - ] - } + assert exc.value.args[0] == ( + "Events/Commands cannot have associations. Remove account (HasOne) from class UserRegistered" + ) diff --git a/tests/event/test_event_meta.py b/tests/event/test_event_meta.py index 805fb046..76db6b25 100644 --- a/tests/event/test_event_meta.py +++ b/tests/event/test_event_meta.py @@ -28,11 +28,10 @@ def test_event_definition_without_aggregate_or_stream(test_domain): with pytest.raises(IncorrectUsageError) as exc: test_domain.register(UserLoggedIn) - assert exc.value.messages == { - "_event": [ - "Event `UserLoggedIn` needs to be associated with an aggregate or a stream" - ] - } + assert ( + exc.value.args[0] + == "Event `UserLoggedIn` needs to be associated with an aggregate or a stream" + ) def test_that_abstract_events_can_be_defined_without_aggregate_or_stream(test_domain): diff --git a/tests/event/tests.py b/tests/event/tests.py index 6b9df9ca..757e2271 100644 --- a/tests/event/tests.py +++ b/tests/event/tests.py @@ -182,7 +182,7 @@ class Dummy: with pytest.raises(IncorrectUsageError) as exc: test_domain.register_external_event(Dummy, "Bar.ExternalEvent.v1") - assert exc.value.messages == {"element": ["Class `Dummy` is not an Event"]} + assert exc.value.args[0] == "Class `Dummy` is not an Event" class TestDomainEventEquivalence: diff --git a/tests/event_sourced_aggregates/test_apply.py b/tests/event_sourced_aggregates/test_apply.py index 461c721e..f40c7680 100644 --- a/tests/event_sourced_aggregates/test_apply.py +++ b/tests/event_sourced_aggregates/test_apply.py @@ -98,9 +98,9 @@ class _(BaseAggregate): def sent(self, event: Sent, _: str) -> None: pass - assert exc.value.messages == { - "_entity": ["Handler method `sent` has incorrect number of arguments"] - } + assert ( + exc.value.args[0] == "Handler method `sent` has incorrect number of arguments" + ) def test_that_apply_decorator_without_event_cls_raises_error(): @@ -117,11 +117,10 @@ class _(BaseAggregate): def sent(self, _: Send) -> None: pass - assert exc.value.messages == { - "_entity": [ - "Apply method `sent` should accept an argument annotated with the Event class" - ] - } + assert ( + exc.value.args[0] + == "Apply method `sent` should accept an argument annotated with the Event class" + ) # Argument should be annotated with pytest.raises(IncorrectUsageError) as exc: @@ -133,11 +132,10 @@ class _(BaseAggregate): def sent(self, _) -> None: pass - assert exc.value.messages == { - "_entity": [ - "Apply method `sent` should accept an argument annotated with the Event class" - ] - } + assert ( + exc.value.args[0] + == "Apply method `sent` should accept an argument annotated with the Event class" + ) # Argument should be supplied with pytest.raises(IncorrectUsageError) as exc: @@ -149,11 +147,10 @@ class _(BaseAggregate): def sent(self) -> None: pass - assert exc.value.messages == { - "_entity": [ - "Apply method `sent` should accept an argument annotated with the Event class" - ] - } + assert ( + exc.value.args[0] + == "Apply method `sent` should accept an argument annotated with the Event class" + ) def test_event_to_be_applied_should_have_a_projection(test_domain): diff --git a/tests/event_sourced_aggregates/test_event_association_with_aggregate.py b/tests/event_sourced_aggregates/test_event_association_with_aggregate.py index 6883d09e..5fc9e7ec 100644 --- a/tests/event_sourced_aggregates/test_event_association_with_aggregate.py +++ b/tests/event_sourced_aggregates/test_event_association_with_aggregate.py @@ -96,12 +96,10 @@ def test_that_trying_to_associate_an_event_with_multiple_aggregates_throws_an_er with pytest.raises(IncorrectUsageError) as exc: test_domain.init(traverse=False) - assert exc.value.messages == { - "_event": [ - "Events are associated with multiple event sourced aggregates: " - "tests.event_sourced_aggregates.test_event_association_with_aggregate.UserRegistered" - ] - } + assert exc.value.args[0] == ( + "Events are associated with multiple event sourced aggregates: " + "tests.event_sourced_aggregates.test_event_association_with_aggregate.UserRegistered" + ) @pytest.mark.eventstore diff --git a/tests/event_sourced_repository/test_add.py b/tests/event_sourced_repository/test_add.py index 912b1456..74df8ac1 100644 --- a/tests/event_sourced_repository/test_add.py +++ b/tests/event_sourced_repository/test_add.py @@ -37,9 +37,7 @@ def test_exception_on_empty_aggregate_object(test_domain): with pytest.raises(IncorrectUsageError) as exception: test_domain.repository_for(User).add(None) - assert exception.value.messages == { - "_entity": ["Aggregate object to persist is invalid"] - } + assert exception.value.args[0] == "Aggregate object to persist is invalid" def test_successful_persistence_of_aggregate(test_domain): diff --git a/tests/event_sourced_repository/test_loading_aggregates.py b/tests/event_sourced_repository/test_loading_aggregates.py index 1b6db49b..d654f3cf 100644 --- a/tests/event_sourced_repository/test_loading_aggregates.py +++ b/tests/event_sourced_repository/test_loading_aggregates.py @@ -115,9 +115,7 @@ def test_fetching_non_existing_aggregates(test_domain): assert exc is not None # FIXME errors should be a list - assert exc.value.messages == { - "_entity": "`User` object with identifier foobar does not exist." - } + assert exc.value.args[0] == "`User` object with identifier foobar does not exist." @pytest.mark.eventstore diff --git a/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py b/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py index ac0a361f..7523dc5d 100644 --- a/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py +++ b/tests/event_sourced_repository/test_retrieving_event_sourced_repository.py @@ -32,11 +32,10 @@ class CustomUserRepository(BaseEventSourcedRepository): with pytest.raises(IncorrectUsageError) as exc: test_domain.register(CustomUserRepository) - assert exc.value.messages == { - "_entity": [ - "Repository `CustomUserRepository` should be associated with an Aggregate" - ] - } + assert ( + exc.value.args[0] + == "Repository `CustomUserRepository` should be associated with an Aggregate" + ) def test_that_an_event_sourced_repository_can_only_be_associated_with_an_event_sourced_aggregate( @@ -52,8 +51,6 @@ class CustomRepository(BaseEventSourcedRepository): test_domain.register(CustomAggregate) test_domain.register(CustomRepository, part_of=CustomAggregate) - assert exc.value.messages == { - "_entity": [ - "Repository `CustomRepository` can only be associated with an Event Sourced Aggregate" - ] - } + assert exc.value.args[0] == ( + "Repository `CustomRepository` can only be associated with an Event Sourced Aggregate" + ) diff --git a/tests/event_store/test_appending_commands.py b/tests/event_store/test_appending_commands.py index f44bda86..afccff93 100644 --- a/tests/event_store/test_appending_commands.py +++ b/tests/event_store/test_appending_commands.py @@ -28,11 +28,10 @@ def test_command_submission_without_aggregate(test_domain): with pytest.raises(IncorrectUsageError) as exc: test_domain.register(Register) - assert exc.value.messages == { - "_command": [ - "Command `Register` needs to be associated with an aggregate or a stream" - ] - } + assert ( + exc.value.args[0] + == "Command `Register` needs to be associated with an aggregate or a stream" + ) @pytest.mark.eventstore diff --git a/tests/field/test_list.py b/tests/field/test_list.py index 36ef06ae..e0cf29f7 100644 --- a/tests/field/test_list.py +++ b/tests/field/test_list.py @@ -108,12 +108,10 @@ class VO(BaseEntity): with pytest.raises(IncorrectUsageError) as exc: List(content_type=ValueObject(VO)) - assert exc.value.messages == { - "_value_object": [ - "`VO` is not a valid Value Object and cannot be embedded in " - "a Value Object field" - ] - } + assert exc.value.args[0] == ( + "`VO` is not a valid Value Object and cannot be embedded in " + "a Value Object field" + ) def test_list_field_with_value_object_string_is_resolved(self, test_domain): class VO(BaseValueObject): diff --git a/tests/field/test_vo.py b/tests/field/test_vo.py index 7f69a387..3bb3da39 100644 --- a/tests/field/test_vo.py +++ b/tests/field/test_vo.py @@ -29,8 +29,6 @@ class User(BaseAggregate): email = String() address = ValueObject(Address) - assert exc.value.messages == { - "_value_object": [ - "`Address` is not a valid Value Object and cannot be embedded in a Value Object field" - ] - } + assert exc.value.args[0] == ( + "`Address` is not a valid Value Object and cannot be embedded in a Value Object field" + ) diff --git a/tests/message/test_message_to_object.py b/tests/message/test_message_to_object.py index 152e988c..5aab8cc6 100644 --- a/tests/message/test_message_to_object.py +++ b/tests/message/test_message_to_object.py @@ -83,5 +83,5 @@ def test_invalid_message_throws_exception(): message.to_object() assert exc.value.messages == { - "_message": ["Message type is not supported for deserialization"] + "kind": ["Message type is not supported for deserialization"] } diff --git a/tests/reflection/test_data_fields.py b/tests/reflection/test_data_fields.py index 4adfe1e0..07d08846 100644 --- a/tests/reflection/test_data_fields.py +++ b/tests/reflection/test_data_fields.py @@ -1,4 +1,7 @@ +import pytest + from protean.core.aggregate import BaseAggregate +from protean.exceptions import IncorrectUsageError from protean.fields import Integer, String from protean.utils.reflection import data_fields @@ -11,3 +14,16 @@ class Person(BaseAggregate): def test_data_fields(): assert len(data_fields(Person)) == 4 assert all(key in data_fields(Person) for key in ["name", "age", "id", "_version"]) + + +def test_data_fields_on_non_element(): + class Dummy: + pass + + with pytest.raises(IncorrectUsageError) as exception: + data_fields(Dummy) + + assert exception.value.args[0] == ( + ".Dummy'> " + "does not have fields" + ) diff --git a/tests/reflection/test_declared_fields.py b/tests/reflection/test_declared_fields.py index 3f253059..6b20d10f 100644 --- a/tests/reflection/test_declared_fields.py +++ b/tests/reflection/test_declared_fields.py @@ -23,9 +23,7 @@ class Dummy: with pytest.raises(IncorrectUsageError) as exception: declared_fields(Dummy) - assert exception.value.messages == { - "field": [ - ".Dummy'> " - "does not have fields" - ] - } + assert exception.value.args[0] == ( + ".Dummy'> " + "does not have fields" + ) diff --git a/tests/reflection/test_fields.py b/tests/reflection/test_fields.py index ae95e347..48415c62 100644 --- a/tests/reflection/test_fields.py +++ b/tests/reflection/test_fields.py @@ -23,9 +23,7 @@ class Dummy: with pytest.raises(IncorrectUsageError) as exception: fields(Dummy) - assert exception.value.messages == { - "field": [ - ".Dummy'> " - "does not have fields" - ] - } + assert exception.value.args[0] == ( + ".Dummy'> " + "does not have fields" + ) diff --git a/tests/repository/test_repository_registration.py b/tests/repository/test_repository_registration.py index 055fa0f5..959d576d 100644 --- a/tests/repository/test_repository_registration.py +++ b/tests/repository/test_repository_registration.py @@ -147,8 +147,6 @@ class CustomUserRepository: def special_method(self): pass - assert exc.value.messages == { - "_entity": [ - "Repository `CustomUserRepository` should be associated with a valid Database" - ] - } + assert exc.value.args[0] == ( + "Repository `CustomUserRepository` should be associated with a valid Database" + ) diff --git a/tests/repository/tests.py b/tests/repository/tests.py index a238de4d..e6e8d0da 100644 --- a/tests/repository/tests.py +++ b/tests/repository/tests.py @@ -37,6 +37,4 @@ def test_that_incorrectusageerror_is_raised_when_retrieving_nonexistent_aggregat with pytest.raises(IncorrectUsageError) as exc: test_domain.repository_for("Invalid") - assert exc.value.messages == { - "element": ["Element Invalid is not registered in domain Test"] - } + assert exc.value.args[0] == ("Element Invalid is not registered in domain Test") diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 9d8e60e8..af6e16b3 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,12 +1,86 @@ import pickle -from protean.exceptions import ObjectNotFoundError +import pytest + +from protean.exceptions import ( + ObjectNotFoundError, + ProteanException, + ProteanExceptionWithMessage, +) def test_pickling_of_exceptions(): - exc = ObjectNotFoundError({"_entity": "foo"}) + exc = ObjectNotFoundError("foo") pickled_exc = pickle.dumps(exc) unpickled_exc = pickle.loads(pickled_exc) - assert exc.messages == unpickled_exc.messages + assert exc.args[0] == unpickled_exc.args[0] + + +class TestProteanException: + @pytest.fixture + def exception_instance(self): + return ProteanException("An error occurred") + + def test_exception_initialization(self, exception_instance): + assert exception_instance.args[0] == "An error occurred" + assert exception_instance.extra_info is None + + def test_exception_with_extra_info(self): + exception_instance = ProteanException( + "An error occurred", extra_info="Extra info" + ) + assert exception_instance.extra_info == "Extra info" + + def test_exception_no_args(self): + exception_instance = ProteanException() + assert exception_instance.args == () + + def test_exception_multiple_args(self): + exception_instance = ProteanException( + "Error 1", "Error 2", extra_info="Extra info" + ) + assert exception_instance.args == ("Error 1", "Error 2") + assert exception_instance.extra_info == "Extra info" + + +class TestProteanExceptionWithMessage: + def test_exception_initialization(self): + messages = {"error": "An error occurred"} + exception_instance = ProteanExceptionWithMessage(messages) + + assert exception_instance.messages == {"error": "An error occurred"} + assert exception_instance.traceback is None + + def test_exception_str(self): + messages = {"error": "An error occurred"} + exception_instance = ProteanExceptionWithMessage(messages) + + assert str(exception_instance) == "{'error': 'An error occurred'}" + + def test_exception_reduce(self): + messages = {"error": "An error occurred"} + exception_instance = ProteanExceptionWithMessage(messages) + + reduced = exception_instance.__reduce__() + assert reduced[0] is ProteanExceptionWithMessage + assert reduced[1] == ({"error": "An error occurred"},) + + def test_exception_with_traceback(self): + messages = {"error": "An error occurred"} + traceback = "Traceback info" + exception_instance = ProteanExceptionWithMessage(messages, traceback=traceback) + + assert exception_instance.traceback == traceback + + def test_exception_with_additional_kwargs(self): + messages = {"error": "An error occurred"} + extra_info = "Extra info" + exception_instance = ProteanExceptionWithMessage( + messages, extra_info=extra_info + ) + + assert exception_instance.messages == messages + assert exception_instance.traceback is None + assert exception_instance.extra_info == extra_info diff --git a/tests/value_object/test_immutability.py b/tests/value_object/test_immutability.py index 20702dce..f93b4df5 100644 --- a/tests/value_object/test_immutability.py +++ b/tests/value_object/test_immutability.py @@ -25,12 +25,9 @@ def test_value_objects_are_immutable(): with pytest.raises(IncorrectUsageError) as exception: balance.currency = "INR" - assert str(exception.value) == str( - { - "_value_object": [ - "Value Objects are immutable and cannot be modified once created" - ] - } + assert ( + str(exception.value) + == "Value Objects are immutable and cannot be modified once created" ) diff --git a/tests/value_object/test_vo_field_properties.py b/tests/value_object/test_vo_field_properties.py index 9a07225c..fdcefb05 100644 --- a/tests/value_object/test_vo_field_properties.py +++ b/tests/value_object/test_vo_field_properties.py @@ -13,12 +13,9 @@ class Balance(BaseValueObject): currency = String(max_length=3, required=True, unique=True) amount = Float(required=True) - assert str(exception.value) == str( - { - "_value_object": [ - "Value Objects cannot contain fields marked 'unique' (field 'currency')" - ] - } + assert ( + str(exception.value) + == "Value Objects cannot contain fields marked 'unique' (field 'currency')" ) @@ -29,12 +26,9 @@ class Balance(BaseValueObject): currency = String(max_length=3, required=True, identifier=True) amount = Float(required=True) - assert str(exception.value) == str( - { - "_value_object": [ - "Value Objects cannot contain fields marked 'identifier' (field 'currency')" - ] - } + assert ( + str(exception.value) + == "Value Objects cannot contain fields marked 'identifier' (field 'currency')" ) @@ -47,10 +41,6 @@ class Address(BaseEntity): class Office(BaseValueObject): addresses = HasMany(Address) - assert str(exception.value) == str( - { - "_value_object": [ - "Value Objects cannot have associations. Remove addresses (HasMany) from class Office" - ] - } + assert str(exception.value) == ( + "Value Objects cannot have associations. Remove addresses (HasMany) from class Office" ) diff --git a/tests/views/test_view_validations_for_fields.py b/tests/views/test_view_validations_for_fields.py index bb949039..48e32589 100644 --- a/tests/views/test_view_validations_for_fields.py +++ b/tests/views/test_view_validations_for_fields.py @@ -28,8 +28,7 @@ class User(BaseView): test_domain.register(User) assert ( - exception.value.messages["_entity"][0] - == "View `User` needs to have at least one identifier" + exception.value.args[0] == "View `User` needs to have at least one identifier" ) @@ -41,7 +40,7 @@ class User(BaseView): email = ValueObject(Email) assert ( - exception.value.messages["_entity"][0] + exception.value.args[0] == "Views can only contain basic field types. Remove email (ValueObject) from class User" ) @@ -54,7 +53,7 @@ class User(BaseView): role = Reference(Role) assert ( - exception.value.messages["_entity"][0] + exception.value.args[0] == "Views can only contain basic field types. Remove role (Reference) from class User" ) @@ -67,6 +66,6 @@ class User(BaseView): role = HasOne(Role) assert ( - exception.value.messages["_entity"][0] + exception.value.args[0] == "Views can only contain basic field types. Remove role (HasOne) from class User" )