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 3 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
20 changes: 17 additions & 3 deletions pydantic/main.py
Expand Up @@ -287,13 +287,28 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
else:
json_encoder = pydantic_encoder
pre_rv_new, post_rv_new = extract_root_validators(namespace)

dedupe_pre_rv = set([])
beezee marked this conversation as resolved.
Show resolved Hide resolved
beezee marked this conversation as resolved.
Show resolved Hide resolved
pre_rv_unique = []
for rv in pre_root_validators + pre_rv_new:
if rv not in dedupe_pre_rv:
dedupe_pre_rv.add(rv)
pre_rv_unique.append(rv)

dedupe_post_rv = set([])
post_rv_unique = []
for rv in post_root_validators + post_rv_new:
if rv not in dedupe_post_rv:
dedupe_post_rv.add(rv)
post_rv_unique.append(rv)

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__': pre_rv_unique,
'__post_root_validators__': post_rv_unique,
'__schema_cache__': {},
'__json_encoder__': staticmethod(json_encoder),
'__custom_root_type__': _custom_root_type,
Expand Down Expand Up @@ -915,7 +930,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
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