Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add to upper function for strings and bytes #4165

Merged
merged 18 commits into from Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4165-satheler.md
@@ -0,0 +1 @@
Add `Config.anystr_upper` and `to_upper` kwarg to constr and conbytes.
2 changes: 2 additions & 0 deletions docs/examples/types_constrained.py
Expand Up @@ -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)$')
Expand Down
3 changes: 3 additions & 0 deletions docs/usage/model_config.md
Expand Up @@ -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`)

satheler marked this conversation as resolved.
Show resolved Hide resolved
**`anystr_lower`**
: whether to make all characters lowercase for str & byte types (default: `False`)

Expand Down
2 changes: 2 additions & 0 deletions docs/usage/types.md
Expand Up @@ -894,6 +894,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
Expand All @@ -905,6 +906,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
Expand Down
1 change: 1 addition & 0 deletions pydantic/config.py
Expand Up @@ -39,6 +39,7 @@ class Extra(str, Enum):
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
Expand Down
9 changes: 9 additions & 0 deletions pydantic/types.py
Expand Up @@ -35,6 +35,7 @@
constr_length_validator,
constr_lower,
constr_strip_whitespace,
constr_upper,
decimal_validator,
float_validator,
frozenset_validator,
Expand Down Expand Up @@ -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
Expand All @@ -341,13 +343,15 @@ 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


def conbytes(
*,
strip_whitespace: bool = False,
to_upper: bool = False,
to_lower: bool = False,
min_length: int = None,
max_length: int = None,
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions pydantic/validators.py
Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'),
],
Expand All @@ -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'),
],
Expand Down
118 changes: 74 additions & 44 deletions tests/test_types.py
Expand Up @@ -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():
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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(
Expand Down