Skip to content

Commit

Permalink
Fix const validations
Browse files Browse the repository at this point in the history
This fixes #620 and #793
  • Loading branch information
hmvp committed Sep 7, 2019
1 parent 7901711 commit 9260975
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 8 deletions.
1 change: 1 addition & 0 deletions changes/794-hmvp.rst
@@ -0,0 +1 @@
Fix const validations for lists
2 changes: 1 addition & 1 deletion pydantic/error_wrappers.py
Expand Up @@ -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}')

Expand Down
16 changes: 9 additions & 7 deletions pydantic/fields.py
Expand Up @@ -106,8 +106,8 @@ def __init__(
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()
Expand Down Expand Up @@ -251,9 +251,8 @@ def _populate_sub_fields(self) -> None: # noqa: C901 (ignore complexity)
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__(
Expand All @@ -270,13 +269,16 @@ def _populate_validators(self) -> None:
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
Expand Down
81 changes: 81 additions & 0 deletions tests/test_main.py
@@ -1,3 +1,4 @@
import json
from enum import Enum
from typing import Any, ClassVar, List, Mapping

Expand Down Expand Up @@ -397,6 +398,86 @@ class Model(BaseModel):
]


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)]


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: [<SubModel b=1>, <SubModel b=2>, <SubModel b=3>]',
'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: [<SubModel b=1>, <SubModel b=2>, <SubModel b=3>]',
'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)

try:
# Fails
Form(field1={'field': 1}, field2=[{'field': 1}])
except ValidationError as e:
# This should not raise an Json error
assert json.dumps(e.json())


class ValidateAssignmentModel(BaseModel):
a: int = 2
b: constr(min_length=1)
Expand Down

0 comments on commit 9260975

Please sign in to comment.