cleaning up _populate_sub_fields, support tuples (#227)

* cleaning up _populate_sub_fields

* support tuples, fix #12

* fix, history and docs

* rename _create_sub_type
This commit is contained in:
Samuel Colvin
2018-07-10 18:45:15 +01:00
committed by GitHub
parent facfbddb91
commit 1dc294015d
5 changed files with 159 additions and 66 deletions
+1
View File
@@ -7,6 +7,7 @@ v0.12.0 (2018-XX-XX)
....................
* add ``by_alias`` argument in ``.dict()`` and ``.json()`` model methods #205
* add Json type support #214
* support tuples #227
v0.11.2 (2018-07-05)
....................
+7 -1
View File
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Union, Set
from typing import Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel
@@ -7,6 +7,9 @@ class Model(BaseModel):
simple_list: list = None
list_of_ints: List[int] = None
simple_tuple: tuple = None
tuple_of_different_types: Tuple[int, float, str, bool] = None
simple_dict: dict = None
dict_str_float: Dict[str, float] = None
@@ -23,3 +26,6 @@ print(Model(list_of_ints=['1', '2', '3']).list_of_ints) # > [1, 2, 3]
print(Model(simple_dict={'a': 1, b'b': 2}).simple_dict) # > {'a': 1, b'b': 2}
print(Model(dict_str_float={'a': 1, b'b': 2}).dict_str_float) # > {'a': 1.0, 'b': 2.0}
print(Model(simple_tuple=[1, 2, 3, 4]).simple_tuple) # > (1, 2, 3, 4)
print(Model(tuple_of_different_types=[1, 2, 3, 4]).tuple_of_different_types) # > (1, 2.0, '3', True)
+8
View File
@@ -111,6 +111,14 @@ class TupleError(PydanticTypeError):
msg_template = 'value is not a valid tuple'
class TupleLengthError(PydanticValueError):
code = 'tuple.length'
msg_template = 'wrong tuple length {actual_length}, expected {expected_length}'
def __init__(self, *, actual_length: int, expected_length: int) -> None:
super().__init__(actual_length=actual_length, expected_length=expected_length)
class AnyStrMinLengthError(PydanticValueError):
code = 'any_str.min_length'
msg_template = 'ensure this value has at least {limit_value} characters'
+68 -46
View File
@@ -1,6 +1,6 @@
import inspect
from enum import Enum, IntEnum
from typing import Any, Callable, List, Mapping, NamedTuple, Set, Type, Union
from typing import Any, Callable, List, Mapping, NamedTuple, Set, Tuple, Type, Union
from . import errors as errors_
from .error_wrappers import ErrorWrapper
@@ -23,6 +23,7 @@ class Shape(IntEnum):
LIST = 2
SET = 3
MAPPING = 4
TUPLE = 5
class Validator(NamedTuple):
@@ -49,7 +50,7 @@ class Schema:
class Field:
__slots__ = (
'type_', 'key_type_', 'sub_fields', 'key_field', 'validators', 'whole_pre_validators', 'whole_post_validators',
'type_', 'sub_fields', 'key_field', 'validators', 'whole_pre_validators', 'whole_post_validators',
'default', 'required', 'model_config', 'name', 'alias', '_schema', 'validate_always', 'allow_none', 'shape',
'class_validators', 'parse_json'
)
@@ -69,7 +70,6 @@ class Field:
self.name: str = name
self.alias: str = alias or name
self.type_: type = type_
self.key_type_: type = None
self.class_validators = class_validators or []
self.validate_always: bool = False
self.sub_fields: List[Field] = None
@@ -171,16 +171,17 @@ class Field:
self.allow_none = True
else:
types_.append(type_)
self.sub_fields = [self.__class__(
type_=t,
class_validators=self.class_validators,
default=self.default,
required=self.required,
allow_none=self.allow_none,
name=f'{self.name}_{display_as_type(t)}',
model_config=self.model_config,
) for t in types_]
elif issubclass(origin, List):
self.sub_fields = [self._create_sub_type(t, f'{self.name}_{display_as_type(t)}') for t in types_]
return
if issubclass(origin, Tuple):
self.shape = Shape.TUPLE
self.sub_fields = [
self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(self.type_.__args__)
]
return
if issubclass(origin, List):
self.type_ = self.type_.__args__[0]
self.shape = Shape.LIST
elif issubclass(origin, Set):
@@ -188,30 +189,24 @@ class Field:
self.shape = Shape.SET
else:
assert issubclass(origin, Mapping)
self.key_type_ = self.type_.__args__[0]
self.key_field = self._create_sub_type(self.type_.__args__[0], 'key_' + self.name)
self.type_ = self.type_.__args__[1]
self.shape = Shape.MAPPING
self.key_field = self.__class__(
type_=self.key_type_,
class_validators=self.class_validators,
default=self.default,
required=self.required,
allow_none=self.allow_none,
name=f'key_{self.name}',
model_config=self.model_config,
)
if not self.sub_fields and _get_type_origin(self.type_):
if _get_type_origin(self.type_):
# type_ has been refined eg. as the type of a List and sub_fields needs to be populated
self.sub_fields = [self.__class__(
type_=self.type_,
class_validators=self.class_validators,
default=self.default,
required=self.required,
allow_none=self.allow_none,
name=f'_{self.name}',
model_config=self.model_config,
)]
self.sub_fields = [self._create_sub_type(self.type_, '_' + self.name)]
def _create_sub_type(self, type_, name):
return self.__class__(
type_=type_,
name=name,
class_validators=self.class_validators,
default=self.default,
required=self.required,
allow_none=self.allow_none,
model_config=self.model_config,
)
def _populate_validators(self):
if not self.sub_fields:
@@ -259,15 +254,13 @@ class Field:
v, errors = self._validate_singleton(v, values, loc, cls)
elif self.shape is Shape.MAPPING:
v, errors = self._validate_mapping(v, values, loc, cls)
elif self.shape is Shape.TUPLE:
v, errors = self._validate_tuple(v, values, loc, cls)
else:
# list or set
if list_like(v):
v, errors = self._validate_sequence(v, values, loc, cls)
if not errors and self.shape is Shape.SET:
v = set(v)
else:
e = errors_.ListError() if self.shape is Shape.LIST else errors_.SetError()
errors = ErrorWrapper(e, loc=loc, config=self.model_config)
v, errors = self._validate_list_set(v, values, loc, cls)
if not errors and self.shape is Shape.SET:
v = set(v)
if not errors and self.whole_post_validators:
v, errors = self._apply_validators(v, values, loc, cls, self.whole_post_validators)
@@ -279,22 +272,51 @@ class Field:
except (ValueError, TypeError) as exc:
return v, ErrorWrapper(exc, loc=loc, config=self.model_config)
def _validate_sequence(self, v, values, loc, cls):
result, errors = [], []
def _validate_list_set(self, v, values, loc, cls):
if not list_like(v):
e = errors_.ListError() if self.shape is Shape.LIST else errors_.SetError()
return v, ErrorWrapper(e, loc=loc, config=self.model_config)
result, errors = [], []
for i, v_ in enumerate(v):
v_loc = *loc, i
single_result, single_errors = self._validate_singleton(v_, values, v_loc, cls)
if single_errors:
errors.append(single_errors)
r, e = self._validate_singleton(v_, values, v_loc, cls)
if e:
errors.append(e)
else:
result.append(single_result)
result.append(r)
if errors:
return v, errors
else:
return result, None
def _validate_tuple(self, v, values, loc, cls):
e = None
if not list_like(v):
e = errors_.TupleError()
else:
actual_length, expected_length = len(v), len(self.sub_fields)
if actual_length != expected_length:
e = errors_.TupleLengthError(actual_length=actual_length, expected_length=expected_length)
if e:
return v, ErrorWrapper(e, loc=loc, config=self.model_config)
result, errors = [], []
for i, (v_, field) in enumerate(zip(v, self.sub_fields)):
v_loc = *loc, i
r, e = field.validate(v_, values, loc=v_loc, cls=cls)
if e:
errors.append(e)
else:
result.append(r)
if errors:
return v, errors
else:
return tuple(result), None
def _validate_mapping(self, v, values, loc, cls):
try:
v_iter = dict_validator(v)
+75 -19
View File
@@ -1,7 +1,7 @@
import re
from decimal import Decimal
from enum import Enum
from typing import Any, Dict, List, Set, Union
from typing import Any, Dict, List, Set, Tuple, Union
import pytest
@@ -223,24 +223,80 @@ def test_dict_key_error():
]
# TODO re-add when implementing better model validators
# def test_all_model_validator():
# class OverModel(BaseModel):
# a: int = ...
#
# def validate_a_pre(self, v):
# return f'{v}1'
#
# def validate_a(self, v):
# assert isinstance(v, int)
# return f'{v}_main'
#
# def validate_a_post(self, v):
# assert isinstance(v, str)
# return f'{v}_post'
#
# m = OverModel(a=1)
# assert m.a == '11_main_post'
def test_tuple():
class Model(BaseModel):
v: Tuple[int, float, bool]
m = Model(v=[1.2, '2.2', 'true'])
assert m.v == (1, 2.2, True)
def test_tuple_more():
class Model(BaseModel):
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)}
def test_tuple_length_error():
class Model(BaseModel):
v: Tuple[int, float, bool]
with pytest.raises(ValidationError) as exc_info:
Model(v=[1, 2])
assert exc_info.value.errors() == [
{
'loc': ('v',),
'msg': 'wrong tuple length 2, expected 3',
'type': 'value_error.tuple.length',
'ctx': {
'actual_length': 2,
'expected_length': 3,
},
},
]
def test_tuple_invalid():
class Model(BaseModel):
v: Tuple[int, float, bool]
with pytest.raises(ValidationError) as exc_info:
Model(v='xxx')
assert exc_info.value.errors() == [
{
'loc': ('v',),
'msg': 'value is not a valid tuple',
'type': 'type_error.tuple',
},
]
def test_tuple_value_error():
class Model(BaseModel):
v: Tuple[int, float, Decimal]
with pytest.raises(ValidationError) as exc_info:
Model(v=['x', 'y', 'x'])
assert exc_info.value.errors() == [
{
'loc': ('v', 0),
'msg': 'value is not a valid integer',
'type': 'type_error.integer',
},
{
'loc': ('v', 1),
'msg': 'value is not a valid float',
'type': 'type_error.float',
},
{
'loc': ('v', 2),
'msg': 'value is not a valid decimal',
'type': 'type_error.decimal',
},
]
def test_recursive_list():