Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aggregate root validation errors #1586

Merged
merged 4 commits into from Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/1586-beezee.md
@@ -0,0 +1 @@
Adjust handling of root validators so that errors are aggregated from _all_ failing root validators, instead of reporting on only the first root validator to fail.
2 changes: 1 addition & 1 deletion docs/usage/validators.md
Expand Up @@ -99,7 +99,7 @@ validation occurs (and are provided with the raw input data), or `pre=False` (th
they're called after field validation.

Field validation will not occur if `pre=True` root validators raise an error. As with field validators,
"post" (i.e. `pre=False`) root validators by default will be called even if field validation fails; this
"post" (i.e. `pre=False`) root validators by default will be called even if prior validators fail; this
behaviour can be changed by setting the `skip_on_failure=True` keyword argument to the validator.
The `values` argument will be a dict containing the values which passed field validation and
field defaults where applicable.
Expand Down
7 changes: 4 additions & 3 deletions pydantic/main.py
Expand Up @@ -42,6 +42,7 @@
generate_model_signature,
lenient_issubclass,
sequence_like,
unique_list,
validate_field_name,
)

Expand Down Expand Up @@ -287,13 +288,14 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
else:
json_encoder = pydantic_encoder
pre_rv_new, post_rv_new = extract_root_validators(namespace)

new_namespace = {
'__config__': config,
'__fields__': fields,
'__field_defaults__': fields_defaults,
'__validators__': vg.validators,
'__pre_root_validators__': pre_root_validators + pre_rv_new,
'__post_root_validators__': post_root_validators + post_rv_new,
'__pre_root_validators__': unique_list(pre_root_validators + pre_rv_new),
'__post_root_validators__': unique_list(post_root_validators + post_rv_new),
'__schema_cache__': {},
'__json_encoder__': staticmethod(json_encoder),
'__custom_root_type__': _custom_root_type,
Expand Down Expand Up @@ -915,7 +917,6 @@ def validate_model( # noqa: C901 (ignore complexity)
values = validator(cls_, values)
except (ValueError, TypeError, AssertionError) as exc:
errors.append(ErrorWrapper(exc, loc=ROOT_KEY))
break

if errors:
return values, fields_set, ValidationError(errors, cls_)
Expand Down
17 changes: 17 additions & 0 deletions pydantic/utils.py
Expand Up @@ -221,6 +221,23 @@ def to_camel(string: str) -> str:
return ''.join(word.capitalize() for word in string.split('_'))


T = TypeVar('T')


def unique_list(input_list: Union[List[T], Tuple[T, ...]]) -> List[T]:
"""
Make a list unique while maintaining order.
"""
result = []
unique_set = set()
for v in input_list:
if v not in unique_set:
unique_set.add(v)
result.append(v)

return result


class PyObjectStr(str):
"""
String class where repr doesn't include quotes. Useful with Representation when you want to return a string
Expand Down
14 changes: 14 additions & 0 deletions tests/test_utils.py
Expand Up @@ -20,6 +20,7 @@
import_string,
lenient_issubclass,
truncate,
unique_list,
)
from pydantic.version import version_info

Expand Down Expand Up @@ -101,6 +102,19 @@ def test_truncate(input_value, output):
assert truncate(input_value, max_len=20) == output


@pytest.mark.parametrize(
'input_value,output',
[
([], []),
([1, 1, 1, 2, 1, 2, 3, 2, 3, 1, 4, 2, 3, 1], [1, 2, 3, 4]),
(['a', 'a', 'b', 'a', 'b', 'c', 'b', 'c', 'a'], ['a', 'b', 'c']),
],
)
def test_unique_list(input_value, output):
assert unique_list(input_value) == output
assert unique_list(unique_list(input_value)) == unique_list(input_value)


def test_value_items():
v = ['a', 'b', 'c']
vi = ValueItems(v, {0, -1})
Expand Down
28 changes: 23 additions & 5 deletions tests/test_validators.py
Expand Up @@ -737,6 +737,7 @@ def test_root_validator():
class Model(BaseModel):
a: int = 1
b: str
c: str

@validator('b')
def repeat_b(cls, v):
Expand All @@ -749,19 +750,36 @@ def example_root_validator(cls, values):
raise ValueError('foobar')
return dict(values, b='changed')

assert Model(a='123', b='bar').dict() == {'a': 123, 'b': 'changed'}
@root_validator
def example_root_validator2(cls, values):
root_val_values.append(values)
if 'snap' in values.get('c', ''):
raise ValueError('foobar2')
return dict(values, c='changed')

assert Model(a='123', b='bar', c='baz').dict() == {'a': 123, 'b': 'changed', 'c': 'changed'}

with pytest.raises(ValidationError) as exc_info:
Model(b='snap dragon')
assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'}]
Model(b='snap dragon', c='snap dragon2')
assert exc_info.value.errors() == [
{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'},
{'loc': ('__root__',), 'msg': 'foobar2', 'type': 'value_error'},
]

with pytest.raises(ValidationError) as exc_info:
Model(a='broken', b='bar')
Model(a='broken', b='bar', c='baz')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]

assert root_val_values == [{'a': 123, 'b': 'barbar'}, {'a': 1, 'b': 'snap dragonsnap dragon'}, {'b': 'barbar'}]
assert root_val_values == [
{'a': 123, 'b': 'barbar', 'c': 'baz'},
{'a': 123, 'b': 'changed', 'c': 'baz'},
{'a': 1, 'b': 'snap dragonsnap dragon', 'c': 'snap dragon2'},
{'a': 1, 'b': 'snap dragonsnap dragon', 'c': 'snap dragon2'},
{'b': 'barbar', 'c': 'baz'},
{'b': 'changed', 'c': 'baz'},
]


def test_root_validator_pre():
Expand Down