Skip to content

Commit

Permalink
Add to upper function for strings and bytes (#4165)
Browse files Browse the repository at this point in the history
* feat: add to upper function for strings and bytes

* docs(changes): add message for change

* fix: add constr upper on types

* fix: add constr upper on types

* feat: add examples and doc usage

* test: add test to upper for types

* chore: apply suggestions from code review

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>

* chore(docs): reorder `anystr_upper` to under `anystr_lower`

* fix(test): adjust parametrizes to constrained bytes upper

* refactor: use pytest parametrize for unify test constrained str upper

* refactor: use pytest parametrize for unify test constrained str lower

* refactor(test): use pytest parametrize for unify test any str upper

* refactor(test): use pytest parametrize for unify test any str lower

* refactor(test): use pytest parametrize for unify test constrained bytes lower

* refactor(test): use pytest parametrize for unify test any str strip whitespace

* refactor(test): change test signatures to improve readability

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
  • Loading branch information
satheler and hramezani committed Aug 8, 2022
1 parent 65a1381 commit d90def3
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 44 deletions.
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`)

**`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 @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions pydantic/config.py
Expand Up @@ -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
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

0 comments on commit d90def3

Please sign in to comment.