From 0a04bbb3384a977c013b2716911276ffb8bd381c Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Wed, 5 Jun 2024 15:55:37 -0700 Subject: [PATCH] Support lists as field value choices --- src/protean/fields/base.py | 28 +++++++++------- src/protean/fields/basic.py | 6 ---- tests/field/test_associations.py | 27 --------------- tests/field/test_choices.py | 57 ++++++++++++++++++++++++++++++++ tests/field/test_field_types.py | 10 ++++-- 5 files changed, 80 insertions(+), 48 deletions(-) delete mode 100644 tests/field/test_associations.py create mode 100644 tests/field/test_choices.py diff --git a/src/protean/fields/base.py b/src/protean/fields/base.py index 42678661..eb752927 100644 --- a/src/protean/fields/base.py +++ b/src/protean/fields/base.py @@ -91,13 +91,6 @@ def __init__( # Set the choices for this field self.choices = choices - if self.choices: - self.choice_dict = {} - for _, member in self.choices.__members__.items(): - if isinstance(member.value, (tuple, list)): - self.choice_dict[member.value[0]] = member.value[1] - else: - self.choice_dict[member.value] = member.value self._validators = validators @@ -251,12 +244,23 @@ def _load(self, value: Any): # If choices exist then validate that value is be one of the choices if self.choices: - value_list = value - if not isinstance(value, (list, tuple)): - value_list = [value] + # Check if self.choices is an Enum + if type(self.choices) not in [list, tuple] and issubclass( + self.choices, enum.Enum + ): + choices = [item.value for item in self.choices] + + # Check if value is an Enum instance + if isinstance(value, self.choices): + value = value.value + else: + choices = self.choices + + value_list = [value] if not isinstance(value, (list, tuple)) else value + for v in value_list: - if v not in self.choice_dict: - self.fail("invalid_choice", value=v, choices=list(self.choice_dict)) + if v not in choices: + self.fail("invalid_choice", value=v, choices=choices) # Cast and Validate the value for this Field value = self._cast_to_type(value) diff --git a/src/protean/fields/basic.py b/src/protean/fields/basic.py index 36ee8b1d..8c0823fd 100644 --- a/src/protean/fields/basic.py +++ b/src/protean/fields/basic.py @@ -40,9 +40,6 @@ def __init__(self, max_length=255, min_length=None, sanitize=True, **kwargs): def _cast_to_type(self, value): """Convert the value to its string representation""" - if value is None: - return value - value = value if isinstance(value, str) else str(value) return bleach.clean(value) if self.sanitize else value @@ -77,9 +74,6 @@ def __init__(self, sanitize=True, **kwargs): def _cast_to_type(self, value): """Convert the value to its string representation""" - if value is None: - return value - value = value if isinstance(value, str) else str(value) return bleach.clean(value) if self.sanitize else value diff --git a/tests/field/test_associations.py b/tests/field/test_associations.py deleted file mode 100644 index b77c4a55..00000000 --- a/tests/field/test_associations.py +++ /dev/null @@ -1,27 +0,0 @@ -from protean.reflection import attributes, declared_fields - -from .elements import Comment, Post - - -class TestReferenceField: - def test_that_reference_field_has_a_shadow_attribute(self): - assert "post_id" in attributes(Comment) - - def test_that_reference_field_does_not_appear_among_fields(self): - assert "post_id" not in declared_fields(Comment) - - -class TestHasOneField: - def test_that_has_one_field_appears_in_fields(self): - assert "meta" in declared_fields(Post) - - def test_that_has_one_field_does_not_appear_in_attributes(self): - assert "meta" not in attributes(Post) - - -class TestHasManyField: - def test_that_has_many_field_appears_in_fields(self): - assert "comments" in declared_fields(Post) - - def test_that_has_many_field_does_not_appear_in_attributes(self): - assert "comments" not in attributes(Post) diff --git a/tests/field/test_choices.py b/tests/field/test_choices.py new file mode 100644 index 00000000..e4e056ab --- /dev/null +++ b/tests/field/test_choices.py @@ -0,0 +1,57 @@ +import pytest + +from enum import Enum + +from protean.exceptions import ValidationError +from protean.fields import String + + +def test_choices_as_enum(): + """Test choices validations for the string field""" + + class StatusChoices(Enum): + """Set of choices for the status""" + + PENDING = "Pending" + SUCCESS = "Success" + ERROR = "Error" + + status = String(max_length=10, choices=StatusChoices) + assert status is not None + + # Test loading a value + assert status._load("Pending") == "Pending" + # Test loading an Enum + assert status._load(StatusChoices.PENDING) == "Pending" + + # Test invalid value + with pytest.raises(ValidationError) as e_info: + status._load("Failure") + + assert e_info.value.messages == { + "unlinked": [ + "Value `'Failure'` is not a valid choice. " + "Must be among ['Pending', 'Success', 'Error']" + ] + } + + +def test_choices_as_list(): + """Test choices validations for the string field""" + + status = String(max_length=10, choices=["Pending", "Success", "Error"]) + assert status is not None + + # Test loading a value + assert status._load("Pending") == "Pending" + + # Test invalid value + with pytest.raises(ValidationError) as e_info: + status._load("Failure") + + assert e_info.value.messages == { + "unlinked": [ + "Value `'Failure'` is not a valid choice. " + "Must be among ['Pending', 'Success', 'Error']" + ] + } diff --git a/tests/field/test_field_types.py b/tests/field/test_field_types.py index fe56adab..a262c4d0 100644 --- a/tests/field/test_field_types.py +++ b/tests/field/test_field_types.py @@ -31,6 +31,10 @@ def test_init(self): name = String(max_length=10) assert name is not None + def test_returns_none_as_it_is(self): + name = String(max_length=10) + assert name._load(None) is None + def test_type_validation(self): """Test type checking validation for the Field""" name = String(max_length=10) @@ -117,9 +121,9 @@ def test_choice(self): class StatusChoices(enum.Enum): """Set of choices for the status""" - PENDING = (0, "Pending") - SUCCESS = (1, "Success") - ERROR = (2, "Error") + PENDING = 0 + SUCCESS = 1 + ERROR = 2 status = Integer(choices=StatusChoices) assert status is not None