V0.32 merge (#852)

* fix(validate-assignment): do not validate extra fields when `vaidate_assignment` is on

* Update history

* Fix `value` vs. `value_`

* Add tests for `value` vs `value_` case

* uprev

* Fix generic required (#742)

* Fix required fields on GenericModel

* lint

* version up

* __post_init__ with inheritance, fix #739 (#740)

* Fix custom Schema on GenericModel fields (#754)

* Fix custom Schema on GenericModel fields

* Add PR#

* uprev

* Fix error messages for Literal types with multiple allowed values (#770)

* Fix error messages for Literal types with multiple allowed values

* Incorporate feedback

* update history
This commit is contained in:
Samuel Colvin
2019-10-01 17:22:05 +01:00
committed by GitHub
parent 8f29837703
commit 6e5a1363cc
11 changed files with 179 additions and 36 deletions
+11 -1
View File
@@ -3,7 +3,17 @@
History
-------
v0.32 (2019-08-08)
v0.32.2 (2019-08-17)
....................
* fix ``__post_init__`` usage with dataclass inheritance, fix #739 by @samuelcolvin
* fix required fields validation on GenericModels classes, #742 by @amitbl
* fix defining custom ``Schema`` on ``GenericModel`` fields, #754 by @amitbl
v0.32.1 (2019-08-08)
....................
* do not validate extra fields when ``validate_assignment`` is on, #724 by @YaraslauZhylko
v0.32 (2019-08-06)
..................
* add model name to ``ValidationError`` error message, #676 by @dmontagu
* **breaking change**: remove ``__getattr__`` and rename ``__values__`` to ``__dict__`` on ``BaseModel``,
+1
View File
@@ -0,0 +1 @@
fix error messages for ``Literal`` types with multiple allowed values
+12 -5
View File
@@ -41,10 +41,12 @@ def _get_validators(cls: Type['DataclassType']) -> Generator[Any, None, None]:
def setattr_validate_assignment(self: 'DataclassType', name: str, value: Any) -> None:
if self.__initialised__:
d = dict(self.__dict__)
d.pop(name)
value, error_ = self.__pydantic_model__.__fields__[name].validate(value, d, loc=name, cls=self.__class__)
if error_:
raise ValidationError([error_], type(self))
d.pop(name, None)
known_field = self.__pydantic_model__.__fields__.get(name, None)
if known_field:
value, error_ = known_field.validate(value, d, loc=name, cls=self.__class__)
if error_:
raise ValidationError([error_], type(self))
object.__setattr__(self, name, value)
@@ -60,9 +62,12 @@ def _process_class(
config: Type['BaseConfig'],
) -> 'DataclassType':
post_init_original = getattr(_cls, '__post_init__', None)
post_init_post_parse = getattr(_cls, '__post_init_post_parse__', None)
if post_init_original and post_init_original.__name__ == '_pydantic_post_init':
post_init_original = None
if not post_init_original:
post_init_original = getattr(_cls, '__post_init_original__', None)
post_init_post_parse = getattr(_cls, '__post_init_post_parse__', None)
def _pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None:
if post_init_original is not None:
@@ -91,6 +96,8 @@ def _process_class(
cls.__initialised__ = False
cls.__validate__ = classmethod(_validate_dataclass)
cls.__get_validators__ = classmethod(_get_validators)
if post_init_original:
cls.__post_init_original__ = post_init_original
if cls.__pydantic_model__.__config__.validate_assignment and not frozen:
cls.__setattr__ = setattr_validate_assignment
+2 -6
View File
@@ -23,7 +23,7 @@ from .class_validators import Validator, make_generic_validator, prep_validators
from .error_wrappers import ErrorWrapper
from .errors import NoneIsNotAllowedError
from .types import Json, JsonWrapper
from .typing import AnyType, Callable, ForwardRef, display_as_type, is_literal_type, literal_values
from .typing import AnyType, Callable, ForwardRef, display_as_type, is_literal_type
from .utils import lenient_issubclass, sequence_like
from .validators import constant_validator, dict_validator, find_validators, validate_json
@@ -311,11 +311,7 @@ class ModelField:
# python 3.7 only, Pattern is a typing object but without sub fields
return
if is_literal_type(self.type_):
values = literal_values(self.type_)
if len(values) > 1:
self.type_ = Union[tuple(Literal[value] for value in values)]
else:
return
return
origin = getattr(self.type_, '__origin__', None)
if origin is None:
# field is not "typing" object eg. Union, Dict, List etc.
+1 -1
View File
@@ -43,7 +43,7 @@ class GenericModel(BaseModel):
model_name = concrete_name(cls, params)
validators = gather_all_validators(cls)
fields: Dict[str, Tuple[Type[Any], Any]] = {
k: (v, cls.__fields__[k].default) for k, v in concrete_type_hints.items() if k in cls.__fields__
k: (v, cls.__fields__[k].field_info) for k, v in concrete_type_hints.items() if k in cls.__fields__
}
created_model = create_model(
model_name=model_name,
+7 -9
View File
@@ -257,15 +257,13 @@ class BaseModel(metaclass=ModelMetaclass):
elif not self.__config__.allow_mutation:
raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment')
elif self.__config__.validate_assignment:
value_, error_ = self.fields[name].validate(value, self.dict(exclude={name}), loc=name)
if error_:
raise ValidationError([error_], type(self))
else:
self.__dict__[name] = value_
self.__fields_set__.add(name)
else:
self.__dict__[name] = value
self.__fields_set__.add(name)
known_field = self.fields.get(name, None)
if known_field:
value, error_ = known_field.validate(value, self.dict(exclude={name}), loc=name)
if error_:
raise ValidationError([error_], type(self))
self.__dict__[name] = value
self.__fields_set__.add(name)
def __getstate__(self) -> 'DictAny':
return {'__dict__': self.__dict__, '__fields_set__': self.__fields_set__}
+24 -3
View File
@@ -37,7 +37,7 @@ from .types import (
conlist,
constr,
)
from .typing import is_callable_type, is_literal_type, is_new_type, literal_values, new_type_supertype
from .typing import Literal, is_callable_type, is_literal_type, is_new_type, literal_values, new_type_supertype
from .utils import lenient_issubclass
if TYPE_CHECKING: # pragma: no cover
@@ -639,8 +639,16 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
if is_new_type(field_type):
field_type = new_type_supertype(field_type)
if is_literal_type(field_type):
# If there were multiple literal values, field.sub_fields would not be falsy
literal_value = literal_values(field_type)[0]
values = literal_values(field_type)
if len(values) > 1:
return field_schema(
multivalue_literal_field_for_schema(values, field),
by_alias=by_alias,
model_name_map=model_name_map,
ref_prefix=ref_prefix,
known_models=known_models,
)
literal_value = values[0]
field_type = type(literal_value)
f_schema['const'] = literal_value
if issubclass(field_type, Enum):
@@ -688,6 +696,19 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
raise ValueError(f'Value not declarable with JSON Schema, field: {field}')
def multivalue_literal_field_for_schema(values: Tuple[Any, ...], field: ModelField) -> ModelField:
return ModelField(
name=field.name,
type_=Union[tuple(Literal[value] for value in values)],
class_validators=field.class_validators,
model_config=field.model_config,
default=field.default,
required=field.required,
alias=field.alias,
field_info=field.field_info,
)
def encode_default(dft: Any) -> Any:
if isinstance(dft, (int, float, str)):
return dft
+56 -1
View File
@@ -6,7 +6,7 @@ from typing import ClassVar, FrozenSet, Optional
import pytest
import pydantic
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel, ValidationError, validator
def test_simple():
@@ -93,6 +93,42 @@ def test_not_validate_assignment():
assert d.a == '7'
def test_validate_assignment_value_change():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config, frozen=False)
class MyDataclass:
a: int
@validator('a')
def double_a(cls, v):
return v * 2
d = MyDataclass(2)
assert d.a == 4
d.a = 3
assert d.a == 6
def test_validate_assignment_extra():
class Config:
validate_assignment = True
@pydantic.dataclasses.dataclass(config=Config, frozen=False)
class MyDataclass:
a: int
d = MyDataclass(1)
assert d.a == 1
d.extra_field = 1.23
assert d.extra_field == 1.23
d.extra_field = 'bye'
assert d.extra_field == 'bye'
def test_post_init():
post_init_called = False
@@ -500,3 +536,22 @@ def test_frozenset_field():
object_under_test = TestFrozenSet(set=test_set)
assert object_under_test.set == test_set
def test_inheritance_post_init():
post_init_called = False
@pydantic.dataclasses.dataclass
class Base:
a: int
def __post_init__(self):
nonlocal post_init_called
post_init_called = True
@pydantic.dataclasses.dataclass
class Child(Base):
b: int
Child(a=1, b=2)
assert post_init_called
+35 -1
View File
@@ -4,7 +4,7 @@ from typing import Any, ClassVar, Dict, Generic, List, Optional, TypeVar, Union
import pytest
from pydantic import BaseModel, ValidationError, root_validator, validator
from pydantic import BaseModel, Field, ValidationError, root_validator, validator
from pydantic.generics import GenericModel, _generic_types_cache
skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above')
@@ -387,3 +387,37 @@ def test_complex_nesting():
item = [{1: 'a', 'a': 'a'}]
model = MyModel[str](item=item)
assert model.item == item
@skip_36
def test_required_value():
T = TypeVar('T')
class MyModel(GenericModel, Generic[T]):
a: int
with pytest.raises(ValidationError) as exc_info:
MyModel[int]()
assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}]
@skip_36
def test_optional_value():
T = TypeVar('T')
class MyModel(GenericModel, Generic[T]):
a: Optional[int] = 1
model = MyModel[int]()
assert model.dict() == {'a': 1}
@skip_36
def test_custom_schema():
T = TypeVar('T')
class MyModel(GenericModel, Generic[T]):
a: int = Field(1, description='Custom')
schema = MyModel[int].schema()
assert schema['properties']['a'].get('description') == 'Custom'
+3 -9
View File
@@ -1788,16 +1788,10 @@ def test_literal_multiple():
assert exc_info.value.errors() == [
{
'loc': ('a_or_b',),
'msg': "unexpected value; permitted: 'a'",
'msg': "unexpected value; permitted: 'a', 'b'",
'type': 'value_error.const',
'ctx': {'given': 'c', 'permitted': ('a',)},
},
{
'loc': ('a_or_b',),
'msg': "unexpected value; permitted: 'b'",
'type': 'value_error.const',
'ctx': {'given': 'c', 'permitted': ('b',)},
},
'ctx': {'given': 'c', 'permitted': ('a', 'b')},
}
]
+27
View File
@@ -123,6 +123,7 @@ def test_validate_pre_error():
class ValidateAssignmentModel(BaseModel):
a: int = 4
b: str = ...
c: int = 0
@validator('b')
def b_length(cls, v, values, **kwargs):
@@ -130,8 +131,13 @@ class ValidateAssignmentModel(BaseModel):
raise ValueError('b too short')
return v
@validator('c')
def double_c(cls, v):
return v * 2
class Config:
validate_assignment = True
extra = Extra.allow
def test_validating_assignment_ok():
@@ -148,6 +154,27 @@ def test_validating_assignment_fail():
p.b = 'x'
def test_validating_assignment_value_change():
p = ValidateAssignmentModel(b='hello', c=2)
assert p.c == 4
p = ValidateAssignmentModel(b='hello')
assert p.c == 0
p.c = 3
assert p.c == 6
def test_validating_assignment_extra():
p = ValidateAssignmentModel(b='hello', extra_field=1.23)
assert p.extra_field == 1.23
p = ValidateAssignmentModel(b='hello')
p.extra_field = 1.23
assert p.extra_field == 1.23
p.extra_field = 'bye'
assert p.extra_field == 'bye'
def test_validating_assignment_dict():
with pytest.raises(ValidationError) as exc_info:
ValidateAssignmentModel(a='x', b='xx')