Make bool_validator strict (#617)

* Make bool_validator strict

* incorporate feedback

* Add RelaxedBool

* Fix position in schema.py

* update BoolError message

* Incorporate feedback

* Update history

* Add changes

* Update docs/index.rst

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

* Update docs/index.rst

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

* Update tests/test_types.py

Co-Authored-By: Samuel Colvin <samcolvin@gmail.com>

* Incorporate feedback

* Update booleans.py

* Remove RelaxedBool

* tweak docs and update changes to new format
This commit is contained in:
dmontagu
2019-08-10 04:00:29 -07:00
committed by Samuel Colvin
parent e6c44ee069
commit 72edca7532
8 changed files with 131 additions and 22 deletions
+1
View File
@@ -0,0 +1 @@
**Breaking Change:** modify parsing behavior for ``bool``
+33
View File
@@ -0,0 +1,33 @@
from pydantic import BaseModel, StrictBool, ValidationError
class BooleansModel(BaseModel):
standard: bool
strict: StrictBool
print(BooleansModel(standard=False, strict=False))
# BooleansModel standard=False strict=False
print(BooleansModel(standard='False', strict=False))
# BooleansModel standard=False strict=False
try:
BooleansModel(standard='False', strict='False')
except ValidationError as e:
print(str(e))
"""
1 validation error
strict
value is not a valid boolean (type=value_error.strictbool)
"""
print(BooleansModel(standard='False', strict=False))
# BooleansModel standard=False strict=False
try:
BooleansModel(standard=[], strict=False)
except ValidationError as e:
print(str(e))
"""
1 validation error
standard
value could not be parsed to a boolean (type=type_error.bool)
"""
+1 -4
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from uuid import UUID
from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, DirectoryPath, EmailStr, FilePath, NameEmail,
NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, StrictBool, UrlStr, conbytes, condecimal,
NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, UrlStr, conbytes, condecimal,
confloat, conint, conlist, constr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, SecretStr, SecretBytes)
@@ -39,8 +39,6 @@ class Model(BaseModel):
email_address: EmailStr = None
email_and_name: NameEmail = None
is_really_a_bool: StrictBool = None
url: UrlStr = None
password: SecretStr = None
@@ -96,7 +94,6 @@ m = Model(
short_list=[1, 2],
email_address='Samuel Colvin <s@muelcolvin.com >',
email_and_name='Samuel Colvin <s@muelcolvin.com >',
is_really_a_bool=True,
url='http://example.com',
password='password',
password_bytes=b'password2',
+23 -3
View File
@@ -580,12 +580,32 @@ Exotic Types
(This script is complete, it should run "as is")
StrictBool
~~~~~~~~~~
Booleans
~~~~~~~~
Unlike normal ``bool`` fields, ``StrictBool`` can be used to required specifically ``True`` or ``False``,
.. warning::
The logic for parsing ``bool`` fields has changed as of version v1.
Prior to v1, ``bool`` parsing never failed, leading to some unexpected results.
The new logic is described below.
A standard ``bool`` field will raise a ``ValidationError`` if the value is not one of the following:
* A valid boolean (i.e., ``True`` or ``False``),
* The integers ``0`` or ``1``,
* a ``str`` which when converted to lower case is one of
``'off', 'f', 'false', 'n', 'no', '1', 'on', 't', 'true', 'y', 'yes'``
* a ``bytes`` which is valid (per the previous rule) when decoded to ``str``
For stricter behavior, ``StrictBool`` can be used to require specifically ``True`` or ``False``;
nothing else is permitted.
Here is a script demonstrating some of these behaviors:
.. literalinclude:: examples/booleans.py
(This script is complete, it should run "as is")
Callable
~~~~~~~~
+4
View File
@@ -55,6 +55,10 @@ class WrongConstantError(PydanticValueError):
return f'unexpected value; permitted: {permitted}'
class BoolError(PydanticTypeError):
msg_template = 'value could not be parsed to a boolean'
class BytesError(PydanticTypeError):
msg_template = 'byte type expected'
+9 -3
View File
@@ -91,7 +91,8 @@ def bytes_validator(v: Any) -> bytes:
raise errors.BytesError()
BOOL_STRINGS = {'1', 'TRUE', 'ON', 'YES'}
BOOL_FALSE = {False, 0, '0', 'off', 'f', 'false', 'n', 'no'}
BOOL_TRUE = {True, 1, '1', 'on', 't', 'true', 'y', 'yes'}
def bool_validator(v: Any) -> bool:
@@ -100,8 +101,13 @@ def bool_validator(v: Any) -> bool:
if isinstance(v, bytes):
v = v.decode()
if isinstance(v, str):
return v.upper() in BOOL_STRINGS
return bool(v)
v = v.lower()
with change_exception(errors.BoolError, TypeError):
if v in BOOL_TRUE:
return True
if v in BOOL_FALSE:
return False
raise errors.BoolError()
def int_validator(v: Any) -> int:
+2 -2
View File
@@ -187,8 +187,8 @@ def test_tuple_more():
simple_tuple: tuple = None
tuple_of_different_types: Tuple[int, float, str, bool] = None
m = Model(simple_tuple=[1, 2, 3, 4], tuple_of_different_types=[1, 2, 3, 4])
assert m.dict() == {'simple_tuple': (1, 2, 3, 4), 'tuple_of_different_types': (1, 2.0, '3', True)}
m = Model(simple_tuple=[1, 2, 3, 4], tuple_of_different_types=[4, 3, 2, 1])
assert m.dict() == {'simple_tuple': (1, 2, 3, 4), 'tuple_of_different_types': (4, 3.0, '2', True)}
def test_tuple_length_error():
+58 -10
View File
@@ -316,22 +316,59 @@ class CheckModel(BaseModel):
max_anystr_length = 10
class BoolCastable:
def __bool__(self) -> bool:
return True
@pytest.mark.parametrize(
'field,value,result',
[
('bool_check', True, True),
('bool_check', False, False),
('bool_check', None, False),
('bool_check', '', False),
('bool_check', 1, True),
('bool_check', 'TRUE', True),
('bool_check', b'TRUE', True),
('bool_check', 'true', True),
('bool_check', '1', True),
('bool_check', '2', False),
('bool_check', 2, True),
('bool_check', 'on', True),
('bool_check', 'y', True),
('bool_check', 'Y', True),
('bool_check', 'yes', True),
('bool_check', 'Yes', True),
('bool_check', 'YES', True),
('bool_check', 'true', True),
('bool_check', 'True', True),
('bool_check', 'TRUE', True),
('bool_check', 'on', True),
('bool_check', 'On', True),
('bool_check', 'ON', True),
('bool_check', '1', True),
('bool_check', 't', True),
('bool_check', 'T', True),
('bool_check', b'TRUE', True),
('bool_check', False, False),
('bool_check', 0, False),
('bool_check', 'n', False),
('bool_check', 'N', False),
('bool_check', 'no', False),
('bool_check', 'No', False),
('bool_check', 'NO', False),
('bool_check', 'false', False),
('bool_check', 'False', False),
('bool_check', 'FALSE', False),
('bool_check', 'off', False),
('bool_check', 'Off', False),
('bool_check', 'OFF', False),
('bool_check', '0', False),
('bool_check', 'f', False),
('bool_check', 'F', False),
('bool_check', b'FALSE', False),
('bool_check', None, ValidationError),
('bool_check', '', ValidationError),
('bool_check', [], ValidationError),
('bool_check', {}, ValidationError),
('bool_check', [1, 2, 3, 4], ValidationError),
('bool_check', {1: 2, 3: 4}, ValidationError),
('bool_check', b'2', ValidationError),
('bool_check', '2', ValidationError),
('bool_check', 2, ValidationError),
('bool_check', b'\x81', ValidationError),
('bool_check', BoolCastable(), ValidationError),
('str_check', 's', 's'),
('str_check', ' s ', 's'),
('str_check', b's', 's'),
@@ -954,6 +991,17 @@ def test_strict_bool():
Model(v=b'1')
def test_bool_unhashable_fails():
class Model(BaseModel):
v: bool
with pytest.raises(ValidationError) as exc_info:
Model(v={})
assert exc_info.value.errors() == [
{'loc': ('v',), 'msg': 'value could not be parsed to a boolean', 'type': 'type_error.bool'}
]
def test_uuid_error():
class Model(BaseModel):
v: UUID