Skip to content

Commit ec99a1d

Browse files
authored
Merge pull request #98 from binary-butterfly/enum-validator-default-case-insensitive
AnyOfValidator/EnumValidator: Set case-sensitive as default (#93)
2 parents 1a3f271 + 71b74de commit ec99a1d

File tree

5 files changed

+212
-77
lines changed

5 files changed

+212
-77
lines changed

docs/03-basic-validators.md

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,13 @@ These values can potentially be of any type, although strings and integers are p
856856
The `AnyOfValidator` is defined with a simple list of allowed values and accepts only values that are part of this list.
857857
The values will be returned as defined in the list.
858858

859-
By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. In that case, the
860-
value will always be returned as it is defined in the list of allowed values (e.g. if the allowed values contain
861-
"Apple", then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be returned).
859+
By default, strings will be matched **case-insensitively**. To change this, set `case_sensitive=True`. The returned
860+
value will always be as defined in the list of allowed values (e.g. if the allowed values contain "Apple" and the
861+
validator is case-insensitive, then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be
862+
returned).
863+
864+
**NOTE:** Prior to version 0.8.0, the validator was NOT case-insensitive by default. The old parameter `case_insensitive`
865+
still exists for compatibility, but is deprecated now and will be removed in a future version.
862866

863867
The list of allowed values may contain mixed types (e.g. `['banana', 123, True, None]`). Also the allowed values can be
864868
specified with any iterable, not just as a list (e.g. as a set or tuple).
@@ -878,18 +882,19 @@ include the list of allowed values (as "allowed_values"), as long as this list i
878882
```python
879883
from validataclass.validators import AnyOfValidator
880884

881-
# Accept a specific list of strings
882-
validator = AnyOfValidator(['apple', 'banana', 'strawberry'])
883-
validator.validate('banana') # will return 'banana'
884-
validator.validate('strawberry') # will return 'strawberry'
885-
validator.validate('pineapple') # will raise ValueNotAllowedError()
886-
validator.validate(1) # will raise InvalidTypeError(expected_type='str')
887-
888-
# Accept strings with case-insensitive matching
889-
validator = AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_insensitive=True)
885+
# Accept a specific list of strings (case-insensitive by default)
886+
validator = AnyOfValidator(['Apple', 'Banana', 'Strawberry'])
890887
validator.validate('apple') # will return 'Apple'
891888
validator.validate('bAnAnA') # will return 'Banana'
892889
validator.validate('STRAWBERRY') # will return 'Strawberry'
890+
validator.validate('pineapple') # will raise ValueNotAllowedError()
891+
validator.validate(1) # will raise InvalidTypeError(expected_type='str')
892+
893+
# Accept strings with case-sensitive matching
894+
validator = AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_sensitive=True)
895+
validator.validate('Banana') # will return 'Banana'
896+
validator.validate('banana') # will raise ValueNotAllowedError
897+
validator.validate('BANANA') # will raise ValueNotAllowedError
893898

894899
# Accept a list of values of mixed types
895900
validator = AnyOfValidator(['banana', 123, True, None])
@@ -916,7 +921,10 @@ The `EnumValidator` is an extended `AnyOfValidator` that uses `Enum` classes ins
916921

917922
It accepts the **values** of the Enum and converts the input value to the according enum **member**.
918923

919-
Strings will be matched case-sensitively by default. To change this, set `case_insensitive=True`.
924+
Strings will be matched **case-insensitively** by default. To change this, set `case_insensitive=True`.
925+
926+
NOTE: Prior to version 0.8.0, the validator was NOT case-insensitive by default. The old parameter "case_insensitive"
927+
still exists for compatibility, but is deprecated now and will be removed in a future version.
920928

921929
By default all values in the Enum are accepted as input. This can be optionally restricted by specifying the
922930
`allowed_values` parameter, which will override the list of allowed values. Values in this list that are not valid for
@@ -951,19 +959,19 @@ class ExampleIntegerEnum(Enum):
951959
BAR = 3
952960
BAZ = -20
953961

954-
# Default: Accept all values of the ExampleStringEnum
962+
# Default: Accept all values of the ExampleStringEnum (case-insensitive)
955963
validator = EnumValidator(ExampleStringEnum)
956964
validator.validate('apple') # will return ExampleStringEnum.APPLE
957-
validator.validate('banana') # will return ExampleStringEnum.BANANA
958-
validator.validate('strawberry') # will return ExampleStringEnum.STRAWBERRY
965+
validator.validate('BANANA') # will return ExampleStringEnum.BANANA
966+
validator.validate('Strawberry') # will return ExampleStringEnum.STRAWBERRY
959967
validator.validate('pineapple') # will raise ValueNotAllowedError
960968
validator.validate(123) # will raise InvalidTypeError(expected_type='str')
961969

962-
# Accept all values of the ExampleStringEnum with case-insensitive string matching
963-
validator = EnumValidator(ExampleStringEnum, case_insensitive=True)
964-
validator.validate('Apple') # will return ExampleStringEnum.APPLE
965-
validator.validate('bAnAnA') # will return ExampleStringEnum.BANANA
966-
validator.validate('STRAWBERRY') # will return ExampleStringEnum.STRAWBERRY
970+
# Accept all values of the ExampleStringEnum, but with case-sensitive matching
971+
validator = EnumValidator(ExampleStringEnum, case_sensitive=True)
972+
validator.validate('apple') # will return ExampleStringEnum.APPLE
973+
validator.validate('Apple') # will raise ValueNotAllowedError
974+
validator.validate('APPLE') # will raise ValueNotAllowedError
967975

968976
# Default: Accept all values of the ExampleIntegerEnum
969977
validator = EnumValidator(ExampleIntegerEnum)

src/validataclass/validators/any_of_validator.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Use of this source code is governed by an MIT-style license that can be found in the LICENSE file.
55
"""
66

7+
import warnings
78
from typing import Any, Iterable, List, Optional, Union
89

910
from validataclass.exceptions import ValueNotAllowedError, InvalidValidatorOptionException
@@ -24,9 +25,13 @@ class AnyOfValidator(Validator):
2425
The types allowed for input data will be automatically determined from the list of allowed values by default, unless
2526
explicitly specified with the parameter 'allowed_types'.
2627
27-
By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`. In that case,
28-
the value will always be returned as it is defined in the list of allowed values (e.g. if the allowed values contain
29-
"Apple", then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be returned).
28+
By default, strings will be matched *case-insensitively*. To change this, set `case_sensitive=True`. The returned
29+
value will always be as defined in the list of allowed values (e.g. if the allowed values contain "Apple" and the
30+
validator is case-insensitive, then "APPLE" and "apple" will be valid input too, but in all cases "Apple" will be
31+
returned).
32+
33+
NOTE: Prior to version 0.8.0, the validator was NOT case-insensitive by default. The old parameter "case_insensitive"
34+
still exists for compatibility, but is deprecated now and will be removed in a future version.
3035
3136
If the input value is not valid (but has the correct type), a ValueNotAllowedError (code='value_not_allowed') will
3237
be raised. This error will include the list of allowed values (as "allowed_values"), as long as this list is not
@@ -36,11 +41,11 @@ class AnyOfValidator(Validator):
3641
Examples:
3742
3843
```
39-
# Accepts "apple", "banana", "strawberry" (but not "APPLE" or "Banana")
44+
# Accepts "apple", "banana", "strawberry" in any capitalization (e.g. "APPLE" is accepted, but returns "apple")
4045
AnyOfValidator(['apple', 'banana', 'strawberry'])
4146
42-
# Accepts the same values, but case-insensitively. Always returns the defined string (e.g. "apple" -> "Apple").
43-
AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_insensitive=True)
47+
# Accepts the same values, but case-sensitively (e.g. "APPLE" is not accepted, only "apple" is)
48+
AnyOfValidator(['Apple', 'Banana', 'Strawberry'], case_sensitive=True)
4449
```
4550
4651
See also: `EnumValidator` (same principle but using Enum classes instead of raw value lists)
@@ -58,23 +63,25 @@ class AnyOfValidator(Validator):
5863
# Types allowed for input data (set by parameter or autodetermined from allowed_values)
5964
allowed_types: List[type] = None
6065

61-
# Check strings case-insensitively
62-
case_insensitive: bool = False
66+
# If set, strings will be matched case-sensitively
67+
case_sensitive: bool = False
6368

6469
def __init__(
6570
self,
6671
allowed_values: Iterable[Any],
6772
*,
6873
allowed_types: Optional[Union[type, Iterable[type]]] = None,
69-
case_insensitive: bool = False,
74+
case_sensitive: Optional[bool] = None,
75+
case_insensitive: Optional[bool] = None,
7076
):
7177
"""
7278
Create an AnyOfValidator with a specified list of allowed values.
7379
7480
Parameters:
7581
allowed_values: List (or any other iterable) of values that are allowed as input (required)
7682
allowed_types: Types that are allowed for input data (default: None, autodetermine types from allowed_values)
77-
case_insensitive: If set, strings will be matched case-insensitively (default: False)
83+
case_sensitive: If set, strings will be matched case-sensitively (default: True)
84+
case_insensitive: DEPRECATED. Validator is case-insensitive by default, use case_sensitive to change this.
7885
"""
7986
# Save list of allowed values
8087
self.allowed_values = list(allowed_values)
@@ -91,7 +98,24 @@ def __init__(
9198
if len(self.allowed_types) == 0:
9299
raise InvalidValidatorOptionException('Parameter "allowed_types" is an empty list (or types could not be autodetermined).')
93100

94-
self.case_insensitive = case_insensitive
101+
# Changed in 0.8.0: The old "case_insensitive" parameter is now deprecated and replaced by a new parameter
102+
# "case_sensitive", which is True by default.
103+
# TODO: For version 1.0, remove the old parameter completely and set a real default value for the new parameter.
104+
if case_sensitive is not None and case_insensitive is not None:
105+
raise InvalidValidatorOptionException(
106+
'Parameters "case_sensitive" and "case_insensitive" (now deprecated) are mutually exclusive.'
107+
)
108+
elif case_insensitive is not None:
109+
warnings.warn(
110+
'The parameter "case_insensitive" is deprecated since version 0.8.0 and will be removed in the future. '
111+
'The AnyOfValidator and EnumValidator are now case-insensitive by default. To change this, set the new '
112+
'parameter "case_sensitive=False" instead.',
113+
DeprecationWarning
114+
)
115+
case_sensitive = not case_insensitive
116+
117+
# Set case_sensitive parameter, defaulting to True.
118+
self.case_sensitive = case_sensitive if case_sensitive is not None else True
95119

96120
def validate(self, input_data: Any, **kwargs) -> Any:
97121
"""
@@ -124,8 +148,8 @@ def _compare_values(self, input_value: Any, allowed_value: Any) -> bool:
124148
if type(input_value) is not type(allowed_value):
125149
return False
126150

127-
# Compare strings case-insensitively (if option is set)
128-
if type(input_value) is str and self.case_insensitive:
151+
# Compare strings case-insensitively (unless case_sensitive option is set)
152+
if type(input_value) is str and not self.case_sensitive:
129153
return input_value.lower() == allowed_value.lower()
130154
else:
131155
return input_value == allowed_value

src/validataclass/validators/enum_validator.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
'T_Enum',
1616
]
1717

18-
# Type variable for type hints in DataclassValidator
18+
# Type variable for type hints in EnumValidator
1919
T_Enum = TypeVar('T_Enum', bound=Enum)
2020

2121

@@ -36,7 +36,10 @@ class EnumValidator(Generic[T_Enum], AnyOfValidator):
3636
The types allowed for input data will be automatically determined from the allowed Enum values by default, unless
3737
explicitly specified with the parameter `allowed_types`.
3838
39-
By default, strings will be matched case-sensitively. To change this, set `case_insensitive=True`.
39+
By default, strings will be matched *case-insensitively*. To change this, set `case_sensitive=True`.
40+
41+
NOTE: Prior to version 0.8.0, the validator was NOT case-insensitive by default. The old parameter "case_insensitive"
42+
still exists for compatibility, but is deprecated now and will be removed in a future version.
4043
4144
If the input value is not valid (but has the correct type), a ValueNotAllowedError (code='value_not_allowed') will
4245
be raised. This error will include the list of allowed values (as "allowed_values"), as long as this list is not
@@ -67,13 +70,16 @@ class EnumValidator(Generic[T_Enum], AnyOfValidator):
6770
# Enum class used to determine the list of allowed values
6871
enum_cls: Type[Enum]
6972

73+
# TODO: For version 1.0, remove the old parameter "case_insensitive" completely and set a real default value for the
74+
# new "case_sensitive" parameter. (See base AnyOfValidator.)
7075
def __init__(
7176
self,
7277
enum_cls: Type[Enum],
7378
*,
7479
allowed_values: Optional[Iterable[Any]] = None,
7580
allowed_types: Optional[Union[type, Iterable[type]]] = None,
76-
case_insensitive: bool = False,
81+
case_sensitive: Optional[bool] = None,
82+
case_insensitive: Optional[bool] = None,
7783
):
7884
"""
7985
Create a EnumValidator for a specified Enum class, optionally with a restricted list of allowed values.
@@ -82,7 +88,8 @@ def __init__(
8288
enum_cls: Enum class to use for validation (required)
8389
allowed_values: List (or iterable) of values from the Enum that are accepted (default: None, all Enum values allowed)
8490
allowed_types: List (or iterable) of types allowed for input data (default: None, autodetermine types from enum values)
85-
case_insensitive: If set, strings will be matched case-insensitively (default: False)
91+
case_sensitive: If set, strings will be matched case-sensitively (default: True)
92+
case_insensitive: DEPRECATED. Validator is case-insensitive by default, use case_sensitive to change this.
8693
"""
8794
# Ensure parameter is an Enum class
8895
if not isinstance(enum_cls, EnumMeta):
@@ -105,6 +112,7 @@ def __init__(
105112
super().__init__(
106113
allowed_values=any_of_values,
107114
allowed_types=allowed_types,
115+
case_sensitive=case_sensitive,
108116
case_insensitive=case_insensitive,
109117
)
110118

tests/validators/any_of_validator_test.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -194,53 +194,84 @@ def test_empty_allowed_values_with_allowed_types():
194194
'allowed_values': [],
195195
}
196196

197-
# Test AnyOfValidator with case-insensitive option
197+
# Test AnyOfValidator with case-sensitive option
198198

199199
@staticmethod
200200
@pytest.mark.parametrize(
201-
'case_insensitive, input_data, expected_result',
201+
'case_sensitive, input_data, expected_result',
202202
[
203203
# Case-sensitive matching
204-
(False, 'Strawberry', 'Strawberry'),
205-
(False, 42, 42),
206-
207-
# Case-insensitive matching
208204
(True, 'Strawberry', 'Strawberry'),
209-
(True, 'STRAWBERRY', 'Strawberry'),
210-
(True, 'strawberry', 'Strawberry'),
211205
(True, 42, 42),
206+
207+
# Case-insensitive matching (default)
208+
(False, 'Strawberry', 'Strawberry'),
209+
(False, 'STRAWBERRY', 'Strawberry'),
210+
(False, 'strawberry', 'Strawberry'),
211+
(False, 42, 42),
212212
]
213213
)
214-
def test_case_insensitive_valid(case_insensitive, input_data, expected_result):
214+
def test_case_sensitive_valid(case_sensitive, input_data, expected_result):
215215
""" Test AnyOfValidator with case-sensitive and case-insensitive string matching, valid input. """
216-
validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_insensitive=case_insensitive)
216+
validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_sensitive=case_sensitive)
217217
assert validator.validate(input_data) == expected_result
218218

219219
@staticmethod
220220
@pytest.mark.parametrize(
221-
'case_insensitive, input_data',
221+
'case_sensitive, input_data',
222222
[
223223
# Case-sensitive matching
224-
(False, 'strawberry'),
225-
(False, 'banana'),
226-
(False, 13),
227-
228-
# Case-insensitive matching
229-
(True, 'straw_berry'),
224+
(True, 'strawberry'),
225+
(True, 'STRAWBERRY'),
230226
(True, 'banana'),
231227
(True, 13),
228+
229+
# Case-insensitive matching (default)
230+
(False, 'straw_berry'),
231+
(False, 'banana'),
232+
(False, 13),
232233
]
233234
)
234-
def test_case_insensitive_invalid(case_insensitive, input_data):
235+
def test_case_sensitive_invalid(case_sensitive, input_data):
235236
""" Test AnyOfValidator with case-sensitive and case-insensitive string matching, invalid input. """
236-
validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_insensitive=case_insensitive)
237+
validator = AnyOfValidator(allowed_values=['Strawberry', 42], case_sensitive=case_sensitive)
237238
with pytest.raises(ValueNotAllowedError) as exception_info:
238239
validator.validate(input_data)
239240
assert exception_info.value.to_dict() == {
240241
'code': 'value_not_allowed',
241242
'allowed_values': ['Strawberry', 42],
242243
}
243244

245+
@staticmethod
246+
@pytest.mark.parametrize(
247+
'case_insensitive, valid_input, invalid_input',
248+
[
249+
# Case-insensitive matching
250+
(True, ['Strawberry', 'strawberry', 'STRAWBERRY'], ['banana']),
251+
252+
# Case-sensitive matching
253+
(False, ['Strawberry'], ['strawberry', 'STRAWBERRY', 'banana']),
254+
]
255+
)
256+
def test_deprecated_case_insensitive(case_insensitive, valid_input, invalid_input):
257+
""" Test AnyOfValidator with deprecated case_insensitive parameter, which should issue a deprecation warning. """
258+
with pytest.deprecated_call():
259+
# This should issue a DeprecationWarning for the case_insensitive parameter, but continue without problems
260+
validator = AnyOfValidator(allowed_values=['Strawberry'], case_insensitive=case_insensitive)
261+
262+
# Valid input
263+
for input_data in valid_input:
264+
assert validator.validate(input_data) == 'Strawberry'
265+
266+
# Invalid input
267+
for input_data in invalid_input:
268+
with pytest.raises(ValueNotAllowedError) as exception_info:
269+
validator.validate(input_data)
270+
assert exception_info.value.to_dict() == {
271+
'code': 'value_not_allowed',
272+
'allowed_values': ['Strawberry'],
273+
}
274+
244275
# Tests for validation errors
245276

246277
@staticmethod
@@ -274,3 +305,20 @@ def test_empty_allowed_types():
274305
with pytest.raises(InvalidValidatorOptionException) as exception_info:
275306
AnyOfValidator([1, 2, 3], allowed_types=[])
276307
assert str(exception_info.value) == 'Parameter "allowed_types" is an empty list (or types could not be autodetermined).'
308+
309+
@staticmethod
310+
@pytest.mark.parametrize(
311+
'case_sensitive, case_insensitive',
312+
[
313+
(True, True),
314+
(True, False),
315+
(False, True),
316+
(False, False),
317+
]
318+
)
319+
def test_case_insensitive_parameter_mutually_exclusive(case_sensitive, case_insensitive):
320+
""" Test that the parameters "case_sensitive" and "case_insensitive" (deprecated) cannot be set at the same time. """
321+
with pytest.raises(InvalidValidatorOptionException) as exception_info:
322+
AnyOfValidator(['banana'], case_sensitive=case_sensitive, case_insensitive=case_insensitive)
323+
assert str(exception_info.value) == \
324+
'Parameters "case_sensitive" and "case_insensitive" (now deprecated) are mutually exclusive.'

0 commit comments

Comments
 (0)