Skip to content

Commit

Permalink
Added StrictBytes type (pydantic#2136)
Browse files Browse the repository at this point in the history
* Added StrictBytes type

* updated tests for StrictBytes, updated relevant sections in documentation for StrictBytes and ConstrainedBytes

* added changelog

* chore: typo in change

Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>
  • Loading branch information
rlizzo and PrettyWood committed Dec 1, 2020
1 parent 35fde4e commit 465f267
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 5 deletions.
1 change: 1 addition & 0 deletions changes/2136-rlizzo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `StrictBytes` type as well as `strict=False` option to `ConstrainedBytes`.
19 changes: 18 additions & 1 deletion docs/examples/types_strict.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
from pydantic import BaseModel, StrictBool, StrictInt, ValidationError, confloat
from pydantic import (
BaseModel,
StrictBytes,
StrictBool,
StrictInt,
ValidationError,
confloat,
)


class StrictBytesModel(BaseModel):
strict_bytes: StrictBytes


try:
StrictBytesModel(strict_bytes='hello world')
except ValidationError as e:
print(e)


class StrictIntModel(BaseModel):
Expand Down
8 changes: 5 additions & 3 deletions docs/usage/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,14 +721,16 @@ Where `Field` refers to the [field function](schema.md#field-customisation).

## Strict Types

You can use the `StrictStr`, `StrictInt`, `StrictFloat`, and `StrictBool` types
You can use the `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool` types
to prevent coercion from compatible types.
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.
This behavior is also exposed via the `strict` field of the `ConstrainedStr`, `ConstrainedBytes`,
`ConstrainedFloat` and `ConstrainedInt` classes and can be combined with a multitude of complex validation rules.

The following caveats apply:

- `StrictBytes` (and the `strict` option of `ConstrainedBytes`) will accept both `bytes`,
and `bytearray` types.
- `StrictInt` (and the `strict` option of `ConstrainedInt`) will not accept `bool` types,
even though `bool` is a subclass of `int` in Python. Other subclasses will work.
- `StrictFloat` (and the `strict` option of `ConstrainedFloat`) will not accept `int`.
Expand Down
1 change: 1 addition & 0 deletions pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
'SecretStr',
'SecretBytes',
'StrictBool',
'StrictBytes',
'StrictInt',
'StrictFloat',
'PaymentCardNumber',
Expand Down
9 changes: 8 additions & 1 deletion pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
path_validator,
set_validator,
str_validator,
strict_bytes_validator,
strict_float_validator,
strict_int_validator,
strict_str_validator,
Expand Down Expand Up @@ -84,6 +85,7 @@
'SecretStr',
'SecretBytes',
'StrictBool',
'StrictBytes',
'StrictInt',
'StrictFloat',
'PaymentCardNumber',
Expand Down Expand Up @@ -112,18 +114,23 @@ class ConstrainedBytes(bytes):
strip_whitespace = False
min_length: OptionalInt = None
max_length: OptionalInt = None
strict: bool = False

@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length)

@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield bytes_validator
yield strict_bytes_validator if cls.strict else bytes_validator
yield constr_strip_whitespace
yield constr_length_validator


class StrictBytes(ConstrainedBytes):
strict = True


def conbytes(*, strip_whitespace: bool = False, min_length: int = None, max_length: int = None) -> Type[bytes]:
# use kwargs then define conf in a dict to aid with IDE type hinting
namespace = dict(strip_whitespace=strip_whitespace, min_length=min_length, max_length=max_length)
Expand Down
9 changes: 9 additions & 0 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ def bytes_validator(v: Any) -> bytes:
raise errors.BytesError()


def strict_bytes_validator(v: Any) -> Union[bytes]:
if isinstance(v, bytes):
return v
elif isinstance(v, bytearray):
return bytes(v)
else:
raise errors.BytesError()


BOOL_FALSE = {0, '0', 'off', 'f', 'false', 'n', 'no'}
BOOL_TRUE = {1, '1', 'on', 't', 'true', 'y', 'yes'}

Expand Down
34 changes: 34 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
SecretBytes,
SecretStr,
StrictBool,
StrictBytes,
StrictFloat,
StrictInt,
StrictStr,
Expand Down Expand Up @@ -1300,6 +1301,39 @@ class Model(BaseModel):
]


def test_strict_bytes():
class Model(BaseModel):
v: StrictBytes

assert Model(v=b'foobar').v == b'foobar'
assert Model(v=bytearray('foobar', 'utf-8')).v == b'foobar'

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

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

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


def test_strict_bytes_subclass():
class MyStrictBytes(StrictBytes):
pass

class Model(BaseModel):
v: MyStrictBytes

a = Model(v=MyStrictBytes(b'foobar'))
assert isinstance(a.v, MyStrictBytes)
assert a.v == b'foobar'

b = Model(v=MyStrictBytes(bytearray('foobar', 'utf-8')))
assert isinstance(b.v, MyStrictBytes)
assert b.v == 'foobar'.encode()


def test_strict_str():
class Model(BaseModel):
v: StrictStr
Expand Down

0 comments on commit 465f267

Please sign in to comment.