diff --git a/docs/03-basic-validators.md b/docs/03-basic-validators.md index 72c6210..c8fd725 100644 --- a/docs/03-basic-validators.md +++ b/docs/03-basic-validators.md @@ -57,6 +57,7 @@ A much more useful distinction is to categorize the validators according to thei - `NoneToUnsetValue`: Like `Noneable`, but converts `None` to `UnsetValue` - `AnythingValidator`: Accepts any input without validation (optionally with type restrictions) - `RejectValidator`: Rejects any input with a validation error (except for `None` if allowed) + - `DiscardValidator`: Discards any input and returns a predefined value - `AllowEmptyString`: Wraps another validator but allows the input to be empty string `('')` @@ -1150,7 +1151,7 @@ validator.validate({13: 12}) # returns {13: 12} ### RejectValidator -The `RejectValidator` is a special validator rejects any input with a validation error. +The `RejectValidator` is a special validator that rejects any input with a validation error. This validator can be used for example in dataclasses to define a field that may never be set, or to override an existing field in a subclassed dataclass that may not be set in this subclass. Keep in mind that in a dataclass @@ -1219,6 +1220,42 @@ validator.validate('foo') # raises CustomValidationError with reason='This fiel ``` +### DiscardValidator + +The `DiscardValidator` is a special validator that discards any input and always returns a predefined value. + +This validator accepts any input of any type, similar to the `AnythingValidator`, but ignores the input entirely and +always returns a predefined value instead. + +By default, the returned value is `None`. This can be changed using the `output_value` parameter. + +This validator should never raise any validation errors, since it doesn't validate anything. + +**Examples:** + +```python +from validataclass.validators import DiscardValidator, Noneable + +# Accepts anything, always returns None +validator = DiscardValidator() +validator.validate(None) # returns None +validator.validate(42) # returns None +validator.validate('') # returns None + +# Accepts anything, always returns the string "discarded" +validator = DiscardValidator(output_value="discarded") +validator.validate(None) # returns "discarded" +validator.validate(42) # returns "discarded" +validator.validate('') # returns "discarded" + +# Use Noneable to allow None, and replace any other input with the string "discarded" +validator = Noneable(DiscardValidator(output_value="discarded")) +validator.validate(None) # returns None +validator.validate(42) # returns "discarded" +validator.validate('') # returns "discarded" +``` + + ### AllowEmptyString The `AllowEmptyString` validator wraps another validator and additionally allows empty string `('')` as an input value. diff --git a/src/validataclass/validators/__init__.py b/src/validataclass/validators/__init__.py index 82fbe80..767d28e 100644 --- a/src/validataclass/validators/__init__.py +++ b/src/validataclass/validators/__init__.py @@ -18,6 +18,7 @@ from .none_to_unset_value import NoneToUnsetValue from .anything_validator import AnythingValidator from .reject_validator import RejectValidator +from .discard_validator import DiscardValidator from .allow_empty_string import AllowEmptyString # Extended type validators diff --git a/src/validataclass/validators/discard_validator.py b/src/validataclass/validators/discard_validator.py new file mode 100644 index 0000000..cf0af33 --- /dev/null +++ b/src/validataclass/validators/discard_validator.py @@ -0,0 +1,60 @@ +""" +validataclass +Copyright (c) 2022, binary butterfly GmbH and contributors +Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. +""" + +from copy import deepcopy +from typing import Any + +from .validator import Validator + +__all__ = [ + 'DiscardValidator', +] + + +class DiscardValidator(Validator): + """ + Special validator that discards any input and always returns a predefined value. + + This validator accepts any input of any type, but ignores it entirely and always returns the same predefined value + instead. + + By default, the returned value is `None`. This can be changed using the `output_value` parameter. + + This validator should never raise any validation errors, since it doesn't validate anything. + + Examples: + + ``` + # Accept anything, always returns None + DiscardValidator() + + # Accepts anything, always returns the string "discarded" + DiscardValidator(output_value='discarded') + ``` + + See also: `RejectValidator`, `AnythingValidator` + + Valid input: Anything + Output: `None` (or output value specified in constructor) + """ + + # Value that is returned by the validator + output_value: Any + + def __init__(self, *, output_value: Any = None): + """ + Create a DiscardValidator. + + Parameters: + output_value: Value of any type that is returned for any input (default: None) + """ + self.output_value = output_value + + def validate(self, input_data: Any, **kwargs) -> Any: + """ + Validate input data. Discards any input and always returns None (or the specified `output_value`). + """ + return deepcopy(self.output_value) diff --git a/tests/validators/discard_validator_test.py b/tests/validators/discard_validator_test.py new file mode 100644 index 0000000..7484b37 --- /dev/null +++ b/tests/validators/discard_validator_test.py @@ -0,0 +1,47 @@ +""" +validataclass +Copyright (c) 2022, binary butterfly GmbH and contributors +Use of this source code is governed by an MIT-style license that can be found in the LICENSE file. +""" + +import pytest + +from validataclass.helpers import UnsetValue +from validataclass.validators import DiscardValidator + + +class DiscardValidatorTest: + """ + Unit tests for the DiscardValidator. + """ + + example_input_data = [None, True, False, 'banana', 42, [], {}] + + @staticmethod + @pytest.mark.parametrize('input_data', example_input_data) + def test_discard_and_return_none(input_data): + """ Test that DiscardValidator accepts anything and always returns None by default. """ + validator = DiscardValidator() + assert validator.validate(input_data) is None + + @staticmethod + @pytest.mark.parametrize('input_data', example_input_data) + @pytest.mark.parametrize('output_value', [None, True, 'discarded value', UnsetValue]) + def test_discard_and_return_custom_value(input_data, output_value): + """ Test that DiscardValidator with output_value parameter accepts anything and always returns the specified value. """ + validator = DiscardValidator(output_value=output_value) + result = validator.validate(input_data) + + assert type(result) is type(output_value) + assert result == output_value + + @staticmethod + def test_output_value_is_deepcopied(): + """ Test that the given output value is deepcopied (e.g. always return a different empty list with `output_value=[]`). """ + validator = DiscardValidator(output_value=[42]) + first_list = validator.validate('banana') + second_list = validator.validate('banana') + + assert first_list == [42] + assert second_list == [42] + assert first_list is not second_list