mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
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:
+11
-1
@@ -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``,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fix error messages for ``Literal`` types with multiple allowed values
|
||||
+12
-5
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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')},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user