diff --git a/src/protean/core/aggregate.py b/src/protean/core/aggregate.py index c027a7c8..07e69769 100644 --- a/src/protean/core/aggregate.py +++ b/src/protean/core/aggregate.py @@ -157,7 +157,7 @@ def element_to_fact_event(element_cls): # Gather all fields defined in the element, except References. # We ignore references in event payloads. attrs = { - key: value + key: value._clone() for key, value in fields(element_cls).items() if not isinstance(value, Reference) } diff --git a/src/protean/fields/association.py b/src/protean/fields/association.py index dc9d5d82..1ac672f6 100644 --- a/src/protean/fields/association.py +++ b/src/protean/fields/association.py @@ -354,6 +354,14 @@ def get_cache_name(self): def has_changed(self): return self.change is not None + def _clone(self) -> "Association": + """ + Clone the field with all its attributes. + + :return: Cloned Field object + """ + return self + class HasOne(Association): """ diff --git a/src/protean/fields/base.py b/src/protean/fields/base.py index 98905340..e95e8eb7 100644 --- a/src/protean/fields/base.py +++ b/src/protean/fields/base.py @@ -216,6 +216,24 @@ def _run_validators(self, value): if errors: raise exceptions.ValidationError(errors) + def _clone(self) -> "Field": + """ + Clone the field with all its attributes. + + :return: Cloned Field object + """ + return self.__class__( + referenced_as=self.referenced_as, + description=self.description, + identifier=self.identifier, + default=self.default, + required=self.required, + unique=self.unique, + choices=self.choices, + validators=self._validators, + error_messages=self.error_messages, + ) + def _load(self, value: Any): """ Load the value for the field, run validators and return the value. diff --git a/tests/field/test_clone.py b/tests/field/test_clone.py new file mode 100644 index 00000000..20d28363 --- /dev/null +++ b/tests/field/test_clone.py @@ -0,0 +1,105 @@ +from unittest.mock import MagicMock + +from protean.fields import Text + + +class TestFieldClone: + def test_clone_basic_field(self): + # Arrange + field = Text( + referenced_as="test_field", + description="Test Text", + identifier=True, + default="default_value", + required=True, + unique=True, + choices=None, + validators=[], + error_messages={"invalid": "Invalid value"}, + ) + + # Act + cloned_field = field._clone() + + # Assert + assert cloned_field is not field, "The cloned field should be a new instance" + assert cloned_field.referenced_as == field.referenced_as + assert cloned_field.description == field.description + assert cloned_field.identifier == field.identifier + assert cloned_field.default == field.default + assert cloned_field.required == field.required + assert cloned_field.unique == field.unique + assert cloned_field.choices == field.choices + assert cloned_field.validators == field.validators + assert cloned_field.error_messages == field.error_messages + + def test_clone_with_choices(self): + # Arrange + choices_mock = MagicMock() + field = Text( + referenced_as="test_field", + description="Test Text", + identifier=False, + default=None, + required=False, + unique=False, + choices=choices_mock, + validators=[], + error_messages={"invalid_choice": "Invalid choice"}, + ) + + # Act + cloned_field = field._clone() + + # Assert + assert cloned_field is not field, "The cloned field should be a new instance" + assert ( + cloned_field.choices == field.choices + ), "Choices should be identical in the clone" + + def test_clone_with_validators(self): + # Arrange + validators = [lambda x: x > 0] + field = Text( + referenced_as="test_field", + description="Test Text", + identifier=False, + default=None, + required=False, + unique=False, + choices=None, + validators=validators, + error_messages={"required": "This field is required"}, + ) + + # Act + cloned_field = field._clone() + + # Assert + assert cloned_field is not field, "The cloned field should be a new instance" + assert ( + cloned_field.validators == field.validators + ), "Validators should be identical in the clone" + + def test_clone_with_default_callable(self): + # Arrange + field = Text( + referenced_as="test_field", + description="Test Text", + identifier=False, + default=lambda: "dynamic_default", + required=False, + unique=False, + choices=None, + validators=[], + error_messages={"invalid": "Invalid value"}, + ) + + # Act + cloned_field = field._clone() + + # Assert + assert cloned_field is not field, "The cloned field should be a new instance" + assert ( + cloned_field.default is field.default + ), "Default callable should be identical in the clone"