Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Field.clone method #452

Merged
merged 1 commit into from
Aug 9, 2024
Merged
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
2 changes: 1 addition & 1 deletion src/protean/core/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions src/protean/fields/association.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
18 changes: 18 additions & 0 deletions src/protean/fields/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
105 changes: 105 additions & 0 deletions tests/field/test_clone.py
Original file line number Diff line number Diff line change
@@ -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"