diff --git a/changes/4165-satheler.md b/changes/4165-satheler.md new file mode 100644 index 0000000000..82ba93abdf --- /dev/null +++ b/changes/4165-satheler.md @@ -0,0 +1 @@ +Add `Config.anystr_upper` and `to_upper` kwarg to constr and conbytes. diff --git a/docs/examples/types_constrained.py b/docs/examples/types_constrained.py index eb258c3f92..fd4e7ca635 100644 --- a/docs/examples/types_constrained.py +++ b/docs/examples/types_constrained.py @@ -22,10 +22,12 @@ class Model(BaseModel): + upper_bytes: conbytes(to_upper=True) lower_bytes: conbytes(to_lower=True) short_bytes: conbytes(min_length=2, max_length=10) strip_bytes: conbytes(strip_whitespace=True) + upper_str: constr(to_upper=True) lower_str: constr(to_lower=True) short_str: constr(min_length=2, max_length=10) regex_str: constr(regex=r'^apple (pie|tart|sandwich)$') diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md index 098b714e13..d25204a3b2 100644 --- a/docs/usage/model_config.md +++ b/docs/usage/model_config.md @@ -25,6 +25,9 @@ _(This script is complete, it should run "as is")_ **`anystr_strip_whitespace`** : whether to strip leading and trailing whitespace for str & byte types (default: `False`) +**`anystr_upper`** +: whether to make all characters uppercase for str & byte types (default: `False`) + **`anystr_lower`** : whether to make all characters lowercase for str & byte types (default: `False`) diff --git a/docs/usage/types.md b/docs/usage/types.md index 7492dde1df..47fa9ec4fa 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -910,6 +910,7 @@ The following arguments are available when using the `condecimal` type function The following arguments are available when using the `constr` type function - `strip_whitespace: bool = False`: removes leading and trailing whitespace +- `to_upper: bool = False`: turns all characters to uppercase - `to_lower: bool = False`: turns all characters to lowercase - `strict: bool = False`: controls type coercion - `min_length: int = None`: minimum length of the string @@ -921,6 +922,7 @@ The following arguments are available when using the `constr` type function The following arguments are available when using the `conbytes` type function - `strip_whitespace: bool = False`: removes leading and trailing whitespace +- `to_upper: bool = False`: turns all characters to uppercase - `to_lower: bool = False`: turns all characters to lowercase - `min_length: int = None`: minimum length of the byte string - `max_length: int = None`: maximum length of the byte string diff --git a/pydantic/config.py b/pydantic/config.py index adb9fd4b21..b7389c559a 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -80,6 +80,7 @@ class ConfigDict(TypedDict, total=False): class BaseConfig: title: Optional[str] = None anystr_lower: bool = False + anystr_upper: bool = False anystr_strip_whitespace: bool = False min_anystr_length: int = 0 max_anystr_length: Optional[int] = None diff --git a/pydantic/types.py b/pydantic/types.py index d93aad1648..e329967d27 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -35,6 +35,7 @@ constr_length_validator, constr_lower, constr_strip_whitespace, + constr_upper, decimal_validator, float_validator, frozenset_validator, @@ -328,6 +329,7 @@ class StrictFloat(ConstrainedFloat): class ConstrainedBytes(bytes): strip_whitespace = False + to_upper = False to_lower = False min_length: OptionalInt = None max_length: OptionalInt = None @@ -341,6 +343,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: def __get_validators__(cls) -> 'CallableGenerator': yield strict_bytes_validator if cls.strict else bytes_validator yield constr_strip_whitespace + yield constr_upper yield constr_lower yield constr_length_validator @@ -348,6 +351,7 @@ def __get_validators__(cls) -> 'CallableGenerator': def conbytes( *, strip_whitespace: bool = False, + to_upper: bool = False, to_lower: bool = False, min_length: int = None, max_length: int = None, @@ -356,6 +360,7 @@ def conbytes( # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( strip_whitespace=strip_whitespace, + to_upper=to_upper, to_lower=to_lower, min_length=min_length, max_length=max_length, @@ -377,6 +382,7 @@ class StrictBytes(ConstrainedBytes): class ConstrainedStr(str): strip_whitespace = False + to_upper = False to_lower = False min_length: OptionalInt = None max_length: OptionalInt = None @@ -397,6 +403,7 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: def __get_validators__(cls) -> 'CallableGenerator': yield strict_str_validator if cls.strict else str_validator yield constr_strip_whitespace + yield constr_upper yield constr_lower yield constr_length_validator yield cls.validate @@ -416,6 +423,7 @@ def validate(cls, value: Union[str]) -> Union[str]: def constr( *, strip_whitespace: bool = False, + to_upper: bool = False, to_lower: bool = False, strict: bool = False, min_length: int = None, @@ -426,6 +434,7 @@ def constr( # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( strip_whitespace=strip_whitespace, + to_upper=to_upper, to_lower=to_lower, strict=strict, min_length=min_length, diff --git a/pydantic/validators.py b/pydantic/validators.py index 897d1bb4a1..3d6e5217ff 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -206,6 +206,10 @@ def anystr_strip_whitespace(v: 'StrBytes') -> 'StrBytes': return v.strip() +def anystr_upper(v: 'StrBytes') -> 'StrBytes': + return v.upper() + + def anystr_lower(v: 'StrBytes') -> 'StrBytes': return v.lower() @@ -488,6 +492,14 @@ def constr_strip_whitespace(v: 'StrBytes', field: 'ModelField', config: 'BaseCon return v +def constr_upper(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': + upper = field.type_.to_upper or config.anystr_upper + if upper: + v = v.upper() + + return v + + def constr_lower(v: 'StrBytes', field: 'ModelField', config: 'BaseConfig') -> 'StrBytes': lower = field.type_.to_lower or config.anystr_lower if lower: @@ -614,6 +626,7 @@ def check(self, config: Type['BaseConfig']) -> bool: [ str_validator, IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'), + IfConfig(anystr_upper, 'anystr_upper'), IfConfig(anystr_lower, 'anystr_lower'), IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'), ], @@ -623,6 +636,7 @@ def check(self, config: Type['BaseConfig']) -> bool: [ bytes_validator, IfConfig(anystr_strip_whitespace, 'anystr_strip_whitespace'), + IfConfig(anystr_upper, 'anystr_upper'), IfConfig(anystr_lower, 'anystr_lower'), IfConfig(anystr_length_validator, 'min_anystr_length', 'max_anystr_length'), ], diff --git a/tests/test_types.py b/tests/test_types.py index d21d45fe22..acf722b463 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -113,20 +113,34 @@ def test_constrained_bytes_too_long(): ] -def test_constrained_bytes_lower_enabled(): +@pytest.mark.parametrize( + 'to_upper, value, result', + [ + (True, b'abcd', b'ABCD'), + (False, b'aBcD', b'aBcD'), + ], +) +def test_constrained_bytes_upper(to_upper, value, result): class Model(BaseModel): - v: conbytes(to_lower=True) + v: conbytes(to_upper=to_upper) - m = Model(v=b'ABCD') - assert m.v == b'abcd' + m = Model(v=value) + assert m.v == result -def test_constrained_bytes_lower_disabled(): +@pytest.mark.parametrize( + 'to_lower, value, result', + [ + (True, b'ABCD', b'abcd'), + (False, b'ABCD', b'ABCD'), + ], +) +def test_constrained_bytes_lower(to_lower, value, result): class Model(BaseModel): - v: conbytes(to_lower=False) + v: conbytes(to_lower=to_lower) - m = Model(v=b'ABCD') - assert m.v == b'ABCD' + m = Model(v=value) + assert m.v == result def test_constrained_bytes_strict_true(): @@ -699,20 +713,34 @@ def test_constrained_str_too_long(): ] -def test_constrained_str_lower_enabled(): +@pytest.mark.parametrize( + 'to_upper, value, result', + [ + (True, 'abcd', 'ABCD'), + (False, 'aBcD', 'aBcD'), + ], +) +def test_constrained_str_upper(to_upper, value, result): class Model(BaseModel): - v: constr(to_lower=True) + v: constr(to_upper=to_upper) - m = Model(v='ABCD') - assert m.v == 'abcd' + m = Model(v=value) + assert m.v == result -def test_constrained_str_lower_disabled(): +@pytest.mark.parametrize( + 'to_lower, value, result', + [ + (True, 'ABCD', 'abcd'), + (False, 'ABCD', 'ABCD'), + ], +) +def test_constrained_str_lower(to_lower, value, result): class Model(BaseModel): - v: constr(to_lower=False) + v: constr(to_lower=to_lower) - m = Model(v='ABCD') - assert m.v == 'ABCD' + m = Model(v=value) + assert m.v == result def test_constrained_str_max_length_0(): @@ -1784,58 +1812,60 @@ def test_uuid_validation(): ] -def test_anystr_strip_whitespace_enabled(): - class Model(BaseModel): - str_check: str - bytes_check: bytes - - class Config: - anystr_strip_whitespace = True - - m = Model(str_check=' 123 ', bytes_check=b' 456 ') - assert m.str_check == '123' - assert m.bytes_check == b'456' - - -def test_anystr_strip_whitespace_disabled(): +@pytest.mark.parametrize( + 'enabled, str_check, bytes_check, result_str_check, result_bytes_check', + [ + (True, ' 123 ', b' 456 ', '123', b'456'), + (False, ' 123 ', b' 456 ', ' 123 ', b' 456 '), + ], +) +def test_anystr_strip_whitespace(enabled, str_check, bytes_check, result_str_check, result_bytes_check): class Model(BaseModel): str_check: str bytes_check: bytes class Config: - anystr_strip_whitespace = False + anystr_strip_whitespace = enabled - m = Model(str_check=' 123 ', bytes_check=b' 456 ') - assert m.str_check == ' 123 ' - assert m.bytes_check == b' 456 ' + m = Model(str_check=str_check, bytes_check=bytes_check) + assert m.str_check == result_str_check + assert m.bytes_check == result_bytes_check -def test_anystr_lower_enabled(): +@pytest.mark.parametrize( + 'enabled, str_check, bytes_check, result_str_check, result_bytes_check', + [(True, 'ABCDefG', b'abCD1Fg', 'ABCDEFG', b'ABCD1FG'), (False, 'ABCDefG', b'abCD1Fg', 'ABCDefG', b'abCD1Fg')], +) +def test_anystr_upper(enabled, str_check, bytes_check, result_str_check, result_bytes_check): class Model(BaseModel): str_check: str bytes_check: bytes class Config: - anystr_lower = True + anystr_upper = enabled - m = Model(str_check='ABCDefG', bytes_check=b'abCD1Fg') + m = Model(str_check=str_check, bytes_check=bytes_check) - assert m.str_check == 'abcdefg' - assert m.bytes_check == b'abcd1fg' + assert m.str_check == result_str_check + assert m.bytes_check == result_bytes_check -def test_anystr_lower_disabled(): +@pytest.mark.parametrize( + 'enabled, str_check, bytes_check, result_str_check, result_bytes_check', + [(True, 'ABCDefG', b'abCD1Fg', 'abcdefg', b'abcd1fg'), (False, 'ABCDefG', b'abCD1Fg', 'ABCDefG', b'abCD1Fg')], +) +def test_anystr_lower(enabled, str_check, bytes_check, result_str_check, result_bytes_check): class Model(BaseModel): str_check: str bytes_check: bytes class Config: - anystr_lower = False + anystr_lower = enabled - m = Model(str_check='ABCDefG', bytes_check=b'abCD1Fg') + m = Model(str_check=str_check, bytes_check=bytes_check) - assert m.str_check == 'ABCDefG' - assert m.bytes_check == b'abCD1Fg' + assert m.str_check == result_str_check + assert m.bytes_check == result_bytes_check @pytest.mark.parametrize(