From 274290cb615cf4c6a78ae0516def606aaefab5e4 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Thu, 22 Sep 2022 12:46:17 +0200 Subject: [PATCH 1/2] AnyOfValidator, EnumValidator: Add case_insensitive option --- docs/03-basic-validators.md | 21 ++++++- .../validators/any_of_validator.py | 55 ++++++++++++++----- .../validators/enum_validator.py | 10 +++- tests/validators/any_of_validator_test.py | 48 +++++++++++++++- tests/validators/enum_validator_test.py | 46 +++++++++++++++- 5 files changed, 159 insertions(+), 21 deletions(-) diff --git a/docs/03-basic-validators.md b/docs/03-basic-validators.md index e3a1d60..3ddef3e 100644 --- a/docs/03-basic-validators.md +++ b/docs/03-basic-validators.md @@ -836,7 +836,11 @@ These values can potentially be of any type, although strings and integers are p ### AnyOfValidator The `AnyOfValidator` is defined with a simple list of allowed values and accepts only values that are part of this list. -The values will be returned unmodified. +The values will be returned as defined in the list. + +By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. In that case, the +value will always be returned as it is defined in the list of allowed values (e.g. if the allowed values contain +"Apple", then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be returned). The list of allowed values may contain mixed types (e.g. `['banana', 123, True, None]`). Also the allowed values can be specified with any iterable, not just as a list (e.g. as a set or tuple). @@ -859,6 +863,12 @@ validator.validate('strawberry') # will return 'strawberry' validator.validate('pineapple') # will raise ValueNotAllowedError() validator.validate(1) # will raise InvalidTypeError(expected_type='str') +# Accept strings with case-insensitive matching +validator = AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_insensitive=True) +validator.validate('apple') # will return 'Apple' +validator.validate('bAnAnA') # will return 'Banana' +validator.validate('STRAWBERRY') # will return 'Strawberry' + # Accept a list of values of mixed types validator = AnyOfValidator(['banana', 123, True, None]) validator.validate('banana') # will return 'banana' @@ -884,6 +894,8 @@ The `EnumValidator` is an extended `AnyOfValidator` that uses `Enum` classes ins It accepts the **values** of the Enum and converts the input value to the according enum **member**. +Strings will be matched case-sensitively by default. To change this, set `case_insensitive=True`. + By default all values in the Enum are accepted as input. This can be optionally restricted by specifying the `allowed_values` parameter, which will override the list of allowed values. Values in this list that are not valid for the Enum will be silently ignored. @@ -892,7 +904,6 @@ If you just want to disallow certain values without manually specifying all of t `allowed_values` parameter as a set and use some set magic. For example, `allowed_values=set(MyEnum) - {MyEnum.BadValue}` would allow all values of `MyEnum` except for `MyEnum.BadValue`. - The types allowed for input data will be automatically determined from the allowed Enum values by default, unless explicitly specified with the parameter `allowed_types`. @@ -923,6 +934,12 @@ validator.validate('strawberry') # will return ExampleStringEnum.STRAWBERRY validator.validate('pineapple') # will raise ValueNotAllowedError validator.validate(123) # will raise InvalidTypeError(expected_type='str') +# Accept all values of the ExampleStringEnum with case-insensitive string matching +validator = EnumValidator(ExampleStringEnum, case_insensitive=True) +validator.validate('Apple') # will return ExampleStringEnum.APPLE +validator.validate('bAnAnA') # will return ExampleStringEnum.BANANA +validator.validate('STRAWBERRY') # will return ExampleStringEnum.STRAWBERRY + # Default: Accept all values of the ExampleIntegerEnum validator = EnumValidator(ExampleIntegerEnum) validator.validate(1) # will return ExampleIntegerEnum.FOO diff --git a/src/validataclass/validators/any_of_validator.py b/src/validataclass/validators/any_of_validator.py index faa3008..fb5828b 100644 --- a/src/validataclass/validators/any_of_validator.py +++ b/src/validataclass/validators/any_of_validator.py @@ -17,23 +17,31 @@ class AnyOfValidator(Validator): """ Validator that checks an input value against a specified list of allowed values. If the value is contained in the - list, the value is returned unmodified. + list, the value is returned. The allowed values can be specified with any iterable (e.g. a list, a set, a tuple, a generator expression, ...). The types allowed for input data will be automatically determined from the list of allowed values by default, unless explicitly specified with the parameter 'allowed_types'. + By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. In that case, + the value will always be returned as it is defined in the list of allowed values (e.g. if the allowed values contain + "Apple", then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be returned). + Examples: ``` + # Accepts "apple", "banana", "strawberry" (but not "APPLE" or "Banana") AnyOfValidator(['apple', 'banana', 'strawberry']) + + # Accepts the same values, but case-insensitively. Always returns the defined string (e.g. "apple" -> "Apple"). + AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_insensitive=True) ``` See also: `EnumValidator` (same principle but using Enum classes instead of raw value lists) Valid input: All values contained in allowed_values - Output: Unmodified input (if valid) + Output: Value as defined in allowed_values """ # Values allowed as input @@ -42,13 +50,23 @@ class AnyOfValidator(Validator): # Types allowed for input data (set by parameter or autodetermined from allowed_values) allowed_types: List[type] = None - def __init__(self, allowed_values: Iterable[Any], *, allowed_types: Optional[Union[type, Iterable[type]]] = None): + # Check strings case-insensitively + case_insensitive: bool = False + + def __init__( + self, + allowed_values: Iterable[Any], + *, + allowed_types: Optional[Union[type, Iterable[type]]] = None, + case_insensitive: bool = False, + ): """ Create an AnyOfValidator with a specified list of allowed values. Parameters: allowed_values: List (or any other iterable) of values that are allowed as input (required) allowed_types: Types that are allowed for input data (default: None, autodetermine types from allowed_values) + case_insensitive: If set, strings will be matched case-insensitively (default: False) """ # Save list of allowed values self.allowed_values = list(allowed_values) @@ -65,9 +83,11 @@ def __init__(self, allowed_values: Iterable[Any], *, allowed_types: Optional[Uni if len(self.allowed_types) == 0: raise InvalidValidatorOptionException('Parameter "allowed_types" is an empty list (or types could not be autodetermined).') + self.case_insensitive = case_insensitive + def validate(self, input_data: Any, **kwargs) -> Any: """ - Validate that input is in the list of allowed values. Returns the value unmodified. + Validate that input is in the list of allowed values. Returns the value (as defined in the list). """ # Special case to allow None as value if None is in the allowed_values list (bypasses _ensure_type()) if None in self.allowed_values and input_data is None: @@ -77,18 +97,23 @@ def validate(self, input_data: Any, **kwargs) -> Any: self._ensure_type(input_data, self.allowed_types) # Check if input is in the list of allowed values - if not self._is_allowed_value(input_data): - raise ValueNotAllowedError() + for allowed_value in self.allowed_values: + if self._compare_values(input_data, allowed_value): + return allowed_value - return input_data + raise ValueNotAllowedError() - def _is_allowed_value(self, input_value: Any): + def _compare_values(self, input_value: Any, allowed_value: Any) -> bool: """ - Checks if an input value is in the list of allowed values. + Returns True if input value and allowed value are equal, in the sense of this validator (e.g. case-insensitively + if that option is set). """ - # Note: We cannot simply use the "in" operator here because it's not fully typesafe for integers and booleans. (See issue #1.) - # (E.g. all of the following expressions are True according to Python: 1 in [True], 0 in [False], True in [1], False in [0]) - for value in self.allowed_values: - if type(input_value) is type(value) and input_value == value: - return True - return False + # We need to make sure the check is typesafe (e.g. because 1 == True and 0 == False) + if type(input_value) is not type(allowed_value): + return False + + # Compare strings case-insensitively (if option is set) + if type(input_value) is str and self.case_insensitive: + return input_value.lower() == allowed_value.lower() + else: + return input_value == allowed_value diff --git a/src/validataclass/validators/enum_validator.py b/src/validataclass/validators/enum_validator.py index d46ef31..3ac2907 100644 --- a/src/validataclass/validators/enum_validator.py +++ b/src/validataclass/validators/enum_validator.py @@ -36,6 +36,8 @@ class EnumValidator(Generic[T_Enum], AnyOfValidator): The types allowed for input data will be automatically determined from the allowed Enum values by default, unless explicitly specified with the parameter `allowed_types`. + By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. + Examples: ``` @@ -67,6 +69,7 @@ def __init__( *, allowed_values: Optional[Iterable[Any]] = None, allowed_types: Optional[Union[type, Iterable[type]]] = None, + case_insensitive: bool = False, ): """ Create a EnumValidator for a specified Enum class, optionally with a restricted list of allowed values. @@ -75,6 +78,7 @@ def __init__( enum_cls: Enum class to use for validation (required) allowed_values: List (or iterable) of values from the Enum that are accepted (default: None, all Enum values allowed) allowed_types: List (or iterable) of types allowed for input data (default: None, autodetermine types from enum values) + case_insensitive: If set, strings will be matched case-insensitively (default: False) """ # Ensure parameter is an Enum class if not isinstance(enum_cls, EnumMeta): @@ -94,7 +98,11 @@ def __init__( any_of_values = enum_values # Initialize base AnyOfValidator - super().__init__(allowed_values=any_of_values, allowed_types=allowed_types) + super().__init__( + allowed_values=any_of_values, + allowed_types=allowed_types, + case_insensitive=case_insensitive, + ) def validate(self, input_data: Any, **kwargs) -> T_Enum: """ diff --git a/tests/validators/any_of_validator_test.py b/tests/validators/any_of_validator_test.py index a227749..f0b1639 100644 --- a/tests/validators/any_of_validator_test.py +++ b/tests/validators/any_of_validator_test.py @@ -32,7 +32,7 @@ def test_string_values_valid(): assert validator.validate('strawberry') == 'strawberry' @staticmethod - @pytest.mark.parametrize('input_data', ['', 'bananana', 'red']) + @pytest.mark.parametrize('input_data', ['', 'bananana', 'red', 'STRAWBERRY']) def test_string_values_invalid_value(input_data): """ Test AnyOfValidator with string value list with invalid input. """ validator = AnyOfValidator(['red apple', 'green apple', 'strawberry']) @@ -95,7 +95,7 @@ def test_mixed_values_valid(): assert validator.validate(None) is None @staticmethod - @pytest.mark.parametrize('input_data', [0, 13, '', 'banana']) + @pytest.mark.parametrize('input_data', [0, 13, '', 'banana', 'STRAWBERRY']) def test_mixed_values_invalid_value(input_data): """ Test AnyOfValidator with allowed values of mixed types with invalid input. """ validator = AnyOfValidator(allowed_values=('strawberry', 42, None)) @@ -168,6 +168,50 @@ def test_with_specified_allowed_type(allowed_types, expected_type_dict, valid_in **expected_type_dict, } + # Test AnyOfValidator with case-insensitive option + + @staticmethod + @pytest.mark.parametrize( + 'case_insensitive, input_data, expected_result', + [ + # Case-sensitive matching + (False, 'Strawberry', 'Strawberry'), + (False, 42, 42), + + # Case-insensitive matching + (True, 'Strawberry', 'Strawberry'), + (True, 'STRAWBERRY', 'Strawberry'), + (True, 'strawberry', 'Strawberry'), + (True, 42, 42), + ] + ) + def test_case_insensitive_valid(case_insensitive, input_data, expected_result): + """ Test AnyOfValidator with case-sensitive and case-insensitive string matching, valid input. """ + validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_insensitive=case_insensitive) + assert validator.validate(input_data) == expected_result + + @staticmethod + @pytest.mark.parametrize( + 'case_insensitive, input_data', + [ + # Case-sensitive matching + (False, 'strawberry'), + (False, 'banana'), + (False, 13), + + # Case-insensitive matching + (True, 'straw_berry'), + (True, 'banana'), + (True, 13), + ] + ) + def test_case_insensitive_invalid(case_insensitive, input_data): + """ Test AnyOfValidator with case-sensitive and case-insensitive string matching, invalid input. """ + validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_insensitive=case_insensitive) + with pytest.raises(ValueNotAllowedError) as exception_info: + validator.validate(input_data) + assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + # Invalid validator parameters @staticmethod diff --git a/tests/validators/enum_validator_test.py b/tests/validators/enum_validator_test.py index 025f89d..2f62b08 100644 --- a/tests/validators/enum_validator_test.py +++ b/tests/validators/enum_validator_test.py @@ -54,7 +54,7 @@ def test_string_enum_valid(): assert validator.validate('strawberry') is UnitTestStringEnum.STRAWBERRY @staticmethod - @pytest.mark.parametrize('input_data', ['', 'bananana', 'APPLE_RED']) + @pytest.mark.parametrize('input_data', ['', 'bananana', 'APPLE_RED', 'STRAWBERRY']) def test_string_enum_invalid_value(input_data): """ Test EnumValidator with string based Enum with invalid enum values. """ validator = EnumValidator(UnitTestStringEnum) @@ -193,6 +193,50 @@ def test_with_specified_allowed_type(allowed_types, expected_type_str, valid_inp 'expected_type': expected_type_str, } + # Test EnumValidator with case-insensitive option + + @staticmethod + @pytest.mark.parametrize( + 'case_insensitive, allowed_values, input_data, expected_result', + [ + # Case-sensitive matching + (False, None, 'red apple', UnitTestStringEnum.APPLE_RED), + (False, [UnitTestStringEnum.STRAWBERRY], 'strawberry', UnitTestStringEnum.STRAWBERRY), + + # Case-insensitive matching + (True, None, 'red apple', UnitTestStringEnum.APPLE_RED), + (True, None, 'RED apple', UnitTestStringEnum.APPLE_RED), + (True, [UnitTestStringEnum.STRAWBERRY], 'strawberry', UnitTestStringEnum.STRAWBERRY), + (True, [UnitTestStringEnum.STRAWBERRY], 'Strawberry', UnitTestStringEnum.STRAWBERRY), + (True, [UnitTestStringEnum.STRAWBERRY], 'STRAWBERRY', UnitTestStringEnum.STRAWBERRY), + ] + ) + def test_case_insensitive_valid(case_insensitive, allowed_values, input_data, expected_result): + """ Test EnumValidator with case-sensitive and case-insensitive string matching, valid input. """ + validator = EnumValidator(UnitTestStringEnum, allowed_values=allowed_values, case_insensitive=case_insensitive) + assert validator.validate(input_data) is expected_result + + @staticmethod + @pytest.mark.parametrize( + 'case_insensitive, allowed_values, input_data', + [ + # Case-sensitive matching + (False, None, 'RED APPLE'), + (False, [UnitTestStringEnum.STRAWBERRY], 'red apple'), + (False, [UnitTestStringEnum.STRAWBERRY], 'STRAWberry'), + + # Case-insensitive matching + (True, None, 'banana'), + (True, [UnitTestStringEnum.STRAWBERRY], 'red apple'), + ] + ) + def test_case_insensitive_invalid(case_insensitive, allowed_values, input_data): + """ Test EnumValidator with case-sensitive and case-insensitive string matching, invalid input. """ + validator = EnumValidator(UnitTestStringEnum, allowed_values=allowed_values, case_insensitive=case_insensitive) + with pytest.raises(ValueNotAllowedError) as exception_info: + validator.validate(input_data) + assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + # Invalid validator parameters @staticmethod From 5425acd23ce4b5bd4f90509787362a8dbd24ef28 Mon Sep 17 00:00:00 2001 From: Lexi Stelter Date: Thu, 22 Sep 2022 13:23:50 +0200 Subject: [PATCH 2/2] AnyOfValidator, EnumValidator: Add list of allowed values to ValueNotAllowedError --- docs/03-basic-validators.md | 13 ++++-- .../exceptions/misc_exceptions.py | 9 +++- .../validators/any_of_validator.py | 14 ++++++- .../validators/enum_validator.py | 4 ++ tests/validators/any_of_validator_test.py | 42 +++++++++++++++++-- tests/validators/enum_validator_test.py | 28 +++++++++---- 6 files changed, 93 insertions(+), 17 deletions(-) diff --git a/docs/03-basic-validators.md b/docs/03-basic-validators.md index 3ddef3e..358394a 100644 --- a/docs/03-basic-validators.md +++ b/docs/03-basic-validators.md @@ -845,12 +845,16 @@ value will always be returned as it is defined in the list of allowed values (e. The list of allowed values may contain mixed types (e.g. `['banana', 123, True, None]`). Also the allowed values can be specified with any iterable, not just as a list (e.g. as a set or tuple). -Like most other validators, the validator will first check the type of input data and will raise an `InvalidTypeError` for types that -are not allowed. Those allowed types will be automatically determined from the list of values by default (e.g. with `['foo', 'bar', 'baz']` -only strings will be accepted, while the mixed type example from above will accept all of `str`, `int`, `bool` and `NoneType`). +Like most other validators, the validator will first check the type of input data and will raise an `InvalidTypeError` +for types that are not allowed. Those allowed types will be automatically determined from the list of values by default +(e.g. with `['foo', 'bar', 'baz']` only strings will be accepted, while the mixed type example from above will accept +all of `str`, `int`, `bool` and `NoneType`). Optionally the allowed types can be explicitly specified using the parameter `allowed_types`. +If the input value is not valid (but has the correct type), a `ValueNotAllowedError` will be raised. This error will +include the list of allowed values (as "allowed_values"), as long as this list is not longer than 20 items. + **Examples:** ```python @@ -907,6 +911,9 @@ would allow all values of `MyEnum` except for `MyEnum.BadValue`. The types allowed for input data will be automatically determined from the allowed Enum values by default, unless explicitly specified with the parameter `allowed_types`. +If the input value is not valid (but has the correct type), a `ValueNotAllowedError` will be raised. This error will +include the list of allowed values (as "allowed_values"), as long as this list is not longer than 20 items. + **Examples:** ```python diff --git a/src/validataclass/exceptions/misc_exceptions.py b/src/validataclass/exceptions/misc_exceptions.py index 8e3b8fe..c042cf4 100644 --- a/src/validataclass/exceptions/misc_exceptions.py +++ b/src/validataclass/exceptions/misc_exceptions.py @@ -4,6 +4,8 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. """ +from typing import Optional + from validataclass.exceptions import ValidationError __all__ = [ @@ -15,7 +17,10 @@ class ValueNotAllowedError(ValidationError): """ - Validation error raised by `AnyOfValidator` and `EnumValidator` when the input value is not an element in the specified list of - allowed values. + Validation error raised by `AnyOfValidator` and `EnumValidator` when the input value is not an element in the + specified list of allowed values. """ code = 'value_not_allowed' + + def __init__(self, *, allowed_values: Optional[list] = None, **kwargs): + super().__init__(allowed_values=allowed_values, **kwargs) diff --git a/src/validataclass/validators/any_of_validator.py b/src/validataclass/validators/any_of_validator.py index fb5828b..63ca429 100644 --- a/src/validataclass/validators/any_of_validator.py +++ b/src/validataclass/validators/any_of_validator.py @@ -28,6 +28,11 @@ class AnyOfValidator(Validator): the value will always be returned as it is defined in the list of allowed values (e.g. if the allowed values contain "Apple", then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be returned). + If the input value is not valid (but has the correct type), a ValueNotAllowedError (code='value_not_allowed') will + be raised. This error will include the list of allowed values (as "allowed_values"), as long as this list is not + longer than 20 items. (This limit is defined in the attribute `max_allowed_values_in_validation_error`, which cannot + be changed via parameters as of now, but can be changed by subclassing the validator and changing the value.) + Examples: ``` @@ -44,6 +49,9 @@ class AnyOfValidator(Validator): Output: Value as defined in allowed_values """ + # If the list of allowed values is longer than this value, do not include allowed values in ValueNotAllowedError + max_allowed_values_in_validation_error: int = 20 + # Values allowed as input allowed_values: List[Any] = None @@ -101,7 +109,11 @@ def validate(self, input_data: Any, **kwargs) -> Any: if self._compare_values(input_data, allowed_value): return allowed_value - raise ValueNotAllowedError() + # Invalid input (only include list of allowed values in validation error if the list is not too long) + if len(self.allowed_values) <= self.max_allowed_values_in_validation_error: + raise ValueNotAllowedError(allowed_values=self.allowed_values) + else: + raise ValueNotAllowedError() def _compare_values(self, input_value: Any, allowed_value: Any) -> bool: """ diff --git a/src/validataclass/validators/enum_validator.py b/src/validataclass/validators/enum_validator.py index 3ac2907..0841fd8 100644 --- a/src/validataclass/validators/enum_validator.py +++ b/src/validataclass/validators/enum_validator.py @@ -38,6 +38,10 @@ class EnumValidator(Generic[T_Enum], AnyOfValidator): By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. + If the input value is not valid (but has the correct type), a ValueNotAllowedError (code='value_not_allowed') will + be raised. This error will include the list of allowed values (as "allowed_values"), as long as this list is not + longer than 20 items. (See `AnyOfValidator`.) + Examples: ``` diff --git a/tests/validators/any_of_validator_test.py b/tests/validators/any_of_validator_test.py index f0b1639..274ef53 100644 --- a/tests/validators/any_of_validator_test.py +++ b/tests/validators/any_of_validator_test.py @@ -38,7 +38,10 @@ def test_string_values_invalid_value(input_data): validator = AnyOfValidator(['red apple', 'green apple', 'strawberry']) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['red apple', 'green apple', 'strawberry'], + } @staticmethod @pytest.mark.parametrize('input_data', [1, 1.234, True, ['red apple']]) @@ -70,7 +73,11 @@ def test_integer_values_invalid_value(input_data): validator = AnyOfValidator({1, -2, 42}) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + + # Sets are unordered, so checking the exception dictionary is a bit more annoying here + exception_dict = exception_info.value.to_dict() + assert exception_dict['code'] == 'value_not_allowed' + assert sorted(exception_dict['allowed_values']) == [-2, 1, 42] @staticmethod @pytest.mark.parametrize('input_data', ['banana', 1.234, True, [1]]) @@ -101,7 +108,10 @@ def test_mixed_values_invalid_value(input_data): validator = AnyOfValidator(allowed_values=('strawberry', 42, None)) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['strawberry', 42, None], + } @staticmethod @pytest.mark.parametrize('input_data', [1.234, True, False, [1], ['strawberry']]) @@ -135,7 +145,10 @@ def test_mixed_values_typesafety(value_list, invalid_input): # Check against "false positives" (e.g. don't confuse integer 1 with boolean True, or 0 with False) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(invalid_input) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': value_list, + } # Test AnyOfValidator with explicit allowed_types parameter @@ -210,6 +223,27 @@ def test_case_insensitive_invalid(case_insensitive, input_data): validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_insensitive=case_insensitive) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['Strawberry', 42], + } + + # Tests for validation errors + + @staticmethod + def test_value_not_allowed_error_with_too_many_allowed_values(): + """ Test that AnyOfValidator does not include list of allowed values in validation error if too long. """ + validator = AnyOfValidator(allowed_values=range(100)) + + # Valid input + assert validator.validate(0) == 0 + assert validator.validate(99) == 99 + + # Invalid input + with pytest.raises(ValueNotAllowedError) as exception_info: + validator.validate(100) + + # Validation error should NOT contain "allowed_values" assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} # Invalid validator parameters diff --git a/tests/validators/enum_validator_test.py b/tests/validators/enum_validator_test.py index 2f62b08..506cbe8 100644 --- a/tests/validators/enum_validator_test.py +++ b/tests/validators/enum_validator_test.py @@ -60,7 +60,10 @@ def test_string_enum_invalid_value(input_data): validator = EnumValidator(UnitTestStringEnum) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['red apple', 'green apple', 'strawberry'], + } @staticmethod @pytest.mark.parametrize('input_data', [1, 1.234, True, ['red apple']]) @@ -91,7 +94,10 @@ def test_integer_enum_invalid_value(input_data): validator = EnumValidator(UnitTestIntegerEnum) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': [1, 42, 13], + } @staticmethod @pytest.mark.parametrize('input_data', ['red apple', 'RED', 1.234, True, [1]]) @@ -121,7 +127,10 @@ def test_mixed_enum_invalid_value(input_data): validator = EnumValidator(UnitTestMixedEnum) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['foo', 42], + } @staticmethod @pytest.mark.parametrize('input_data', [1.234, True, [1], ['foo']]) @@ -153,7 +162,10 @@ def test_string_enum_allowed_values_invalid(input_data): validator = EnumValidator(UnitTestStringEnum, allowed_values=['red apple', UnitTestStringEnum.APPLE_GREEN, 'banana']) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['red apple', 'green apple'], + } @staticmethod def test_string_enum_allowed_values_as_set(): @@ -235,7 +247,10 @@ def test_case_insensitive_invalid(case_insensitive, allowed_values, input_data): validator = EnumValidator(UnitTestStringEnum, allowed_values=allowed_values, case_insensitive=case_insensitive) with pytest.raises(ValueNotAllowedError) as exception_info: validator.validate(input_data) - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'} + assert exception_info.value.to_dict() == { + 'code': 'value_not_allowed', + 'allowed_values': ['red apple', 'green apple', 'strawberry'] if allowed_values is None else ['strawberry'], + } # Invalid validator parameters @@ -262,6 +277,5 @@ def test_value_error_in_validate(): validator = EnumValidator(UnitTestStringEnum) validator.allowed_values.append('bananana') - with pytest.raises(ValueNotAllowedError) as exception_info: + with pytest.raises(ValueNotAllowedError): validator.validate('bananana') - assert exception_info.value.to_dict() == {'code': 'value_not_allowed'}