Skip to content
This repository has been archived by the owner on Apr 10, 2023. It is now read-only.

Commit

Permalink
Improve ValidationError messages for container fields
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Mar 5, 2020
1 parent dbcffea commit 7746732
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 11 deletions.
1 change: 1 addition & 0 deletions RELEASES.rst
Expand Up @@ -6,6 +6,7 @@ Releases

*Unreleased*

- Improve ``ValidationError`` messages for container fields.
- Rename ``BaseField`` to ``_Base``. (`c5abc2f`_)
- Add ``frozenset`` to FIELD_CLASS_MAP. (`24d7c0e`_)
- Add ``Flatten`` field. (`9c740a5`_)
Expand Down
4 changes: 4 additions & 0 deletions src/serde/exceptions.py
Expand Up @@ -4,6 +4,8 @@

from contextlib import contextmanager

import six


__all__ = [
'ContextError',
Expand Down Expand Up @@ -77,6 +79,8 @@ def messages(self):
# Avoids tags which might not have `_serde_name`
if isinstance(field, Field):
d = {field._serde_name: d}
elif isinstance(field, (six.string_types, six.integer_types)):
d = {field: d}
return d

def __str__(self):
Expand Down
35 changes: 26 additions & 9 deletions src/serde/fields.py
Expand Up @@ -11,7 +11,7 @@
from six import PY3, binary_type, integer_types, text_type
from six.moves.collections_abc import Mapping as MappingType

from serde.exceptions import ContextError, ValidationError
from serde.exceptions import ContextError, ValidationError, add_context
from serde.utils import is_subclass, try_lookup, zip_equal


Expand Down Expand Up @@ -564,7 +564,7 @@ def _apply(self, stage, element):
"""
Apply a stage to a particular element in the container.
"""
return getattr(self.element, stage)(element)
raise NotImplementedError()

def serialize(self, value):
"""
Expand Down Expand Up @@ -643,8 +643,9 @@ def _apply(self, stage, element):
"""
Apply the key stage to each key, and the value stage to each value.
"""
k, v = element
return (getattr(self.key, stage)(k), getattr(self.value, stage)(v))
key, value = element
with add_context(key):
return (getattr(self.key, stage)(key), getattr(self.value, stage)(value))


class Dict(_Mapping):
Expand Down Expand Up @@ -697,13 +698,21 @@ def _iter(self, value):
Iterate over the sequence.
"""
try:
for element in value:
for element in enumerate(value):
yield element
except TypeError:
raise ValidationError(
'invalid type, expected {!r}'.format(self.ty.__name__), value=value
)

def _apply(self, stage, element):
"""
Apply a stage to a particular element in the container.
"""
index, value = element
with add_context(index):
return getattr(self.element, stage)(value)


class Deque(_Sequence):
"""
Expand Down Expand Up @@ -805,20 +814,28 @@ def __init__(self, *elements, **kwargs):
self.elements = tuple(
_resolve_to_field_instance(e, none_allowed=False) for e in elements
)
assert not hasattr(self, 'element')

def _iter(self, value):
"""
Iterate over the fields and each element in the tuple.
"""
return zip_equal(self.elements, super(Tuple, self)._iter(value))
try:
for element in zip_equal(self.elements, super(Tuple, self)._iter(value)):
yield element
except ValueError:
raise ValidationError(
'invalid length, expected {} elements'.format(
len(self.elements), value=value
)
)

def _apply(self, stage, element):
"""
Apply the element field stage to the corresponding element value.
"""
f, v = element
return getattr(f, stage)(v)
field, (index, value) = element
with add_context(index):
return getattr(field, stage)(value)


def create_primitive(name, ty):
Expand Down
18 changes: 16 additions & 2 deletions tests/test_fields.py
Expand Up @@ -578,7 +578,7 @@ class Example(Model):
}

def test_integrate_contained_validators(self):
# An optional with extra validators shoudl be able to be contained by
# An optional with extra validators should be able to be contained by
# other container fields.

class Example(Model):
Expand All @@ -589,7 +589,7 @@ class Example(Model):

with raises(ValidationError) as e:
Example(a=['a', 'b', None, 'c', 'hello there'])
assert e.value.messages() == {'a': 'expected length 1'}
assert e.value.messages() == {'a': {4: 'expected length 1'}}


class TestInstance:
Expand Down Expand Up @@ -684,6 +684,10 @@ def test__iter(self):
with raises(NotImplementedError):
_Container(dict)._iter(object())

def test__apply(self):
with raises(NotImplementedError):
_Container(dict)._apply('_serialize', object())


class TestMapping:
def test___init___basic(self):
Expand Down Expand Up @@ -1165,6 +1169,16 @@ def test_validate_extra(self):
with raises(ValidationError):
field.validate((20, 11))

def test_integrate_incorrect_length(self):
# A Tuple should handle incorrect length inputs.

class Example(Model):
a = Tuple(Int, Str, Int)

with raises(ValidationError) as e:
Example.from_dict({'a': (1, 'testing...')})
assert e.value.messages() == {'a': 'invalid length, expected 3 elements'}


class TestLiteral:
def test___init___basic(self):
Expand Down

0 comments on commit 7746732

Please sign in to comment.