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

Added generic approach to strict type checking for constrained types #799

Merged
merged 14 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/799-DerRidda.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added ability to validate strictness to ``ConstrainedFloat``, ``ConstrainedInt`` and ``ConstrainedStr`` and added
``StrictFloat`` and ``StrictInt`` classes.
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,14 @@ The SecretStr and SecretBytes will be formatted as either `'**********'` or `''`

(This script is complete, it should run "as is")

Strict Types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move the "StrictBool" section into here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to remove the "StrictBool" section: https://pydantic-docs.helpmanual.io/#strictbool

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

............

You can use the ``StrictStr``, ``StrictInt`` and ``StrictFloat`` types to prevent coercion from compatible types.
DerRidda marked this conversation as resolved.
Show resolved Hide resolved
These types will only pass validation when the validated value is of the respective type or is a subtype of that type.
This behavior is also exposed via the ``strict`` field of the ``ConstrainedStr``, ``ConstrainedFloat`` and
``ConstrainedInt`` classes and can be combined with a multitude of complex validation rules.
DerRidda marked this conversation as resolved.
Show resolved Hide resolved
DerRidda marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add a code example demonstrating this behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Json Type
.........

Expand Down
54 changes: 38 additions & 16 deletions pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
decimal_validator,
float_validator,
int_validator,
make_arbitrary_type_validator,
not_none_validator,
number_multiple_validator,
number_size_validator,
Expand Down Expand Up @@ -63,6 +64,8 @@
'SecretStr',
'SecretBytes',
'StrictBool',
'StrictInt',
'StrictFloat',
]

NoneStr = Optional[str]
Expand All @@ -82,18 +85,6 @@
ModelOrDc = Type[Union['BaseModel', 'DataclassType']]


class StrictStr(str):
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate

@classmethod
def validate(cls, v: Any) -> str:
if not isinstance(v, str):
raise errors.StrError()
return v


class ConstrainedBytes(bytes):
strip_whitespace = False
min_length: OptionalInt = None
Expand Down Expand Up @@ -156,9 +147,12 @@ class ConstrainedStr(str):
max_length: OptionalInt = None
curtail_length: OptionalInt = None
regex: Optional[Pattern[str]] = None
strict = False

@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
if cls.strict:
yield make_arbitrary_type_validator(str)
yield not_none_validator
yield str_validator
yield constr_strip_whitespace
Expand All @@ -180,6 +174,7 @@ def validate(cls, value: str) -> str:
def constr(
*,
strip_whitespace: bool = False,
strict: bool = False,
min_length: int = None,
max_length: int = None,
curtail_length: int = None,
Expand All @@ -188,6 +183,7 @@ def constr(
# use kwargs then define conf in a dict to aid with IDE type hinting
namespace = dict(
strip_whitespace=strip_whitespace,
strict=strict,
min_length=min_length,
max_length=max_length,
curtail_length=curtail_length,
Expand All @@ -196,6 +192,10 @@ def constr(
return type('ConstrainedStrValue', (ConstrainedStr,), namespace)


class StrictStr(ConstrainedStr):
strict = True


class StrictBool(int):
"""
StrictBool to allow for bools which are not type-coerced.
Expand Down Expand Up @@ -253,6 +253,7 @@ def __new__(cls, name: str, bases: Any, dct: Dict[str, Any]) -> 'ConstrainedInt'


class ConstrainedInt(int, metaclass=ConstrainedNumberMeta):
strict: bool = False
gt: OptionalInt = None
ge: OptionalInt = None
lt: OptionalInt = None
Expand All @@ -261,14 +262,18 @@ class ConstrainedInt(int, metaclass=ConstrainedNumberMeta):

@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
if cls.strict:
yield make_arbitrary_type_validator(int)
DerRidda marked this conversation as resolved.
Show resolved Hide resolved
yield int_validator
yield number_size_validator
yield number_multiple_validator


def conint(*, gt: int = None, ge: int = None, lt: int = None, le: int = None, multiple_of: int = None) -> Type[int]:
def conint(
*, strict: bool = False, gt: int = None, ge: int = None, lt: int = None, le: int = None, multiple_of: int = None
) -> Type[int]:
# use kwargs then define conf in a dict to aid with IDE type hinting
namespace = dict(gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of)
namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of)
return type('ConstrainedIntValue', (ConstrainedInt,), namespace)


Expand All @@ -280,7 +285,12 @@ class NegativeInt(ConstrainedInt):
lt = 0


class StrictInt(ConstrainedInt):
strict = True


class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta):
strict: bool = False
gt: OptionalIntFloat = None
ge: OptionalIntFloat = None
lt: OptionalIntFloat = None
Expand All @@ -289,16 +299,24 @@ class ConstrainedFloat(float, metaclass=ConstrainedNumberMeta):

@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
if cls.strict:
yield make_arbitrary_type_validator(float)
yield float_validator
yield number_size_validator
yield number_multiple_validator


def confloat(
*, gt: float = None, ge: float = None, lt: float = None, le: float = None, multiple_of: float = None
*,
strict: bool = False,
gt: float = None,
ge: float = None,
lt: float = None,
le: float = None,
multiple_of: float = None,
) -> Type[float]:
# use kwargs then define conf in a dict to aid with IDE type hinting
namespace = dict(gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of)
namespace = dict(strict=strict, gt=gt, ge=ge, lt=lt, le=le, multiple_of=multiple_of)
return type('ConstrainedFloatValue', (ConstrainedFloat,), namespace)


Expand All @@ -310,6 +328,10 @@ class NegativeFloat(ConstrainedFloat):
lt = 0


class StrictFloat(ConstrainedFloat):
strict = True


class ConstrainedDecimal(Decimal, metaclass=ConstrainedNumberMeta):
gt: OptionalIntFloatDecimal = None
ge: OptionalIntFloatDecimal = None
Expand Down
28 changes: 28 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
SecretBytes,
SecretStr,
StrictBool,
StrictFloat,
StrictInt,
StrictStr,
ValidationError,
conbytes,
Expand Down Expand Up @@ -955,6 +957,32 @@ class Model(BaseModel):
Model(v=b'1')


def test_strict_int():
class Model(BaseModel):
v: StrictInt

assert Model(v=123456).v == 123456

with pytest.raises(ValidationError):
DerRidda marked this conversation as resolved.
Show resolved Hide resolved
Model(v='123456')

with pytest.raises(ValidationError):
Model(v=3.14159)


def test_strict_float():
class Model(BaseModel):
v: StrictFloat

assert Model(v=3.14159).v == 3.14159

with pytest.raises(ValidationError):
Model(v='3.14159')

with pytest.raises(ValidationError):
Model(v=123456)
DerRidda marked this conversation as resolved.
Show resolved Hide resolved


def test_bool_unhashable_fails():
class Model(BaseModel):
v: bool
Expand Down