From dd32a4381462bbba105eb50a6b95b357a0e87c81 Mon Sep 17 00:00:00 2001 From: Hmvp Date: Tue, 17 Sep 2019 19:30:58 +0200 Subject: [PATCH] Fix const validations (#794) This fixes #620 and #793 --- changes/794-hmvp.rst | 1 + pydantic/error_wrappers.py | 2 +- pydantic/fields.py | 16 +++--- tests/test_main.py | 106 +++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 changes/794-hmvp.rst diff --git a/changes/794-hmvp.rst b/changes/794-hmvp.rst new file mode 100644 index 0000000..0626c35 --- /dev/null +++ b/changes/794-hmvp.rst @@ -0,0 +1 @@ +Fix const validations for lists diff --git a/pydantic/error_wrappers.py b/pydantic/error_wrappers.py index 15ed6c7..edbe319 100644 --- a/pydantic/error_wrappers.py +++ b/pydantic/error_wrappers.py @@ -105,7 +105,7 @@ def flatten_errors( else: yield error.dict(config, loc_prefix=loc) elif isinstance(error, list): - yield from flatten_errors(error, config) + yield from flatten_errors(error, config, loc=loc) else: raise RuntimeError(f'Unknown error object: {error}') diff --git a/pydantic/fields.py b/pydantic/fields.py index 47104f3..b3337e0 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -106,8 +106,8 @@ class Field: self.sub_fields: Optional[List[Field]] = None self.key_field: Optional[Field] = None self.validators: 'ValidatorsList' = [] - self.whole_pre_validators: Optional['ValidatorsList'] = None - self.whole_post_validators: Optional['ValidatorsList'] = None + self.whole_pre_validators: 'ValidatorsList' = [] + self.whole_post_validators: 'ValidatorsList' = [] self.parse_json: bool = False self.shape: int = SHAPE_SINGLETON self.prepare() @@ -253,9 +253,8 @@ class Field: else: raise TypeError(f'Fields of type "{origin}" are not supported.') - if getattr(self.type_, '__origin__', None): - # type_ has been refined eg. as the type of a List and sub_fields needs to be populated - self.sub_fields = [self._create_sub_type(self.type_, '_' + self.name)] + # type_ has been refined eg. as the type of a List and sub_fields needs to be populated + self.sub_fields = [self._create_sub_type(self.type_, '_' + self.name)] def _create_sub_type(self, type_: AnyType, name: str, *, for_keys: bool = False) -> 'Field': return self.__class__( @@ -272,13 +271,16 @@ class Field: v_funcs = ( *[v.func for v in class_validators_ if not v.whole and v.pre], *(get_validators() if get_validators else list(find_validators(self.type_, self.model_config))), - self.schema is not None and self.schema.const and constant_validator, *[v.func for v in class_validators_ if not v.whole and not v.pre], ) self.validators = self._prep_vals(v_funcs) + # Add const validator + if self.schema is not None and self.schema.const: + self.whole_pre_validators = self._prep_vals([constant_validator]) + if class_validators_: - self.whole_pre_validators = self._prep_vals(v.func for v in class_validators_ if v.whole and v.pre) + self.whole_pre_validators.extend(self._prep_vals(v.func for v in class_validators_ if v.whole and v.pre)) self.whole_post_validators = self._prep_vals(v.func for v in class_validators_ if v.whole and not v.pre) @staticmethod diff --git a/tests/test_main.py b/tests/test_main.py index 019776f..3d859d3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -397,6 +397,112 @@ def test_const_with_wrong_value(): ] +def test_const_list(): + class SubModel(BaseModel): + b: int + + class Model(BaseModel): + a: List[SubModel] = Schema([SubModel(b=1), SubModel(b=2), SubModel(b=3)], const=True) + b: List[SubModel] = Schema([{'b': 4}, {'b': 5}, {'b': 6}], const=True) + + m = Model() + assert m.a == [SubModel(b=1), SubModel(b=2), SubModel(b=3)] + assert m.b == [SubModel(b=4), SubModel(b=5), SubModel(b=6)] + assert m.schema() == { + 'definitions': { + 'SubModel': { + 'properties': {'b': {'title': 'B', 'type': 'integer'}}, + 'required': ['b'], + 'title': 'SubModel', + 'type': 'object', + } + }, + 'properties': { + 'a': { + 'const': [SubModel(b=1), SubModel(b=2), SubModel(b=3)], + 'items': {'$ref': '#/definitions/SubModel'}, + 'title': 'A', + 'type': 'array', + }, + 'b': { + 'const': [{'b': 4}, {'b': 5}, {'b': 6}], + 'items': {'$ref': '#/definitions/SubModel'}, + 'title': 'B', + 'type': 'array', + }, + }, + 'title': 'Model', + 'type': 'object', + } + + +def test_const_list_with_wrong_value(): + class SubModel(BaseModel): + b: int + + class Model(BaseModel): + a: List[SubModel] = Schema([SubModel(b=1), SubModel(b=2), SubModel(b=3)], const=True) + b: List[SubModel] = Schema([{'b': 4}, {'b': 5}, {'b': 6}], const=True) + + with pytest.raises(ValidationError) as exc_info: + Model(a=[{'b': 3}, {'b': 1}, {'b': 2}], b=[{'b': 6}, {'b': 5}]) + + assert exc_info.value.errors() == [ + { + 'ctx': { + 'given': [{'b': 3}, {'b': 1}, {'b': 2}], + 'permitted': [[SubModel(b=1), SubModel(b=2), SubModel(b=3)]], + }, + 'loc': ('a',), + 'msg': 'unexpected value; permitted: [, , ]', + 'type': 'value_error.const', + }, + { + 'ctx': {'given': [{'b': 6}, {'b': 5}], 'permitted': [[{'b': 4}, {'b': 5}, {'b': 6}]]}, + 'loc': ('b',), + 'msg': "unexpected value; permitted: [{'b': 4}, {'b': 5}, {'b': 6}]", + 'type': 'value_error.const', + }, + ] + + with pytest.raises(ValidationError) as exc_info: + Model(a=[SubModel(b=3), SubModel(b=1), SubModel(b=2)], b=[SubModel(b=3), SubModel(b=1)]) + + assert exc_info.value.errors() == [ + { + 'ctx': { + 'given': [SubModel(b=3), SubModel(b=1), SubModel(b=2)], + 'permitted': [[SubModel(b=1), SubModel(b=2), SubModel(b=3)]], + }, + 'loc': ('a',), + 'msg': 'unexpected value; permitted: [, , ]', + 'type': 'value_error.const', + }, + { + 'ctx': {'given': [SubModel(b=3), SubModel(b=1)], 'permitted': [[{'b': 4}, {'b': 5}, {'b': 6}]]}, + 'loc': ('b',), + 'msg': "unexpected value; permitted: [{'b': 4}, {'b': 5}, {'b': 6}]", + 'type': 'value_error.const', + }, + ] + + +def test_const_validation_json_serializable(): + class SubForm(BaseModel): + field: int + + class Form(BaseModel): + field1: SubForm = Schema({'field': 2}, const=True) + field2: List[SubForm] = Schema([{'field': 2}], const=True) + + with pytest.raises(ValidationError) as exc_info: + # Fails + Form(field1={'field': 1}, field2=[{'field': 1}]) + + # This should not raise an Json error + exc_info.value.json() + + class ValidateAssignmentModel(BaseModel): a: int = 2 b: constr(min_length=1)