mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
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:
@@ -0,0 +1 @@
|
||||
**Breaking Change:** modify parsing behavior for ``bool``
|
||||
@@ -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)
|
||||
"""
|
||||
@@ -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
@@ -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
|
||||
~~~~~~~~
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user