mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
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:
@@ -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)
|
||||
....................
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user