diff --git a/src/serde/fields.py b/src/serde/fields.py index 9a249b1..f8d7d5e 100644 --- a/src/serde/fields.py +++ b/src/serde/fields.py @@ -715,80 +715,101 @@ def deserialize(self, d): return self.type.from_dict(d) -class Dict(Instance): +class _Container(Instance): """ - This field represents the built-in `dict` type. - - Each key and value will be serialized, deserialized, normalized, and - validated with the specified key and value types. The key and value types - can be specified using `Field` classes, `Field` instances, `~serde.Model` - classes, or built-in types that have a corresponding field type in this - library. - - Args: - key: the `Field` class or instance for keys in this `Dict`. - value: the `Field` class or instance for values in this `Dict`. - **kwargs: keyword arguments for the `Field` constructor. + A base class for `Dict`, `List`, `Tuple`, and other container fields. """ - type = dict + def _iter(self, value): + """ + Iterate over the container. + """ + return value - def __init__(self, key=None, value=None, **kwargs): + def _apply(self, stage, element): """ - Create a new `Dict`. + Apply a stage to a particular element in the container. """ - super(Dict, self).__init__(self.__class__.type, **kwargs) - self.key = _resolve_to_field_instance(key) - self.value = _resolve_to_field_instance(value) + return getattr(self.element, stage)(element) def serialize(self, value): """ - Serialize the given dictionary. + Serialize the given container. - Each key and value in the dictionary will be serialized with the - specified key and value field instances. + Each element in the container will be serialized with the specified + field instances. """ - return self.type( - (self.key._serialize(k), self.value._serialize(v)) - for k, v in value.items() - ) + value = self.type(self._apply('_serialize', element) for element in self._iter(value)) + return super(_Container, self).serialize(value) def deserialize(self, value): """ - Deserialize the given dictionary. + Deserialize the given container. - Each key and value in the dictionary will be deserialized with the - specified key and value field instances. + Each element in the container will be deserialized with the specified + field instances. """ - return self.type( - (self.key._deserialize(k), self.value._deserialize(v)) - for k, v in value.items() - ) + value = super(_Container, self).deserialize(value) + return self.type(self._apply('_deserialize', element) for element in self._iter(value)) def normalize(self, value): """ - Normalize the given dictionary. + Deserialize the given container. - Each key and value in the dictionary will be normalized with the - specified key and value field instances. + Each element in the container will be normalized with the specified + field instances. """ - return self.type( - (self.key._normalize(k), self.value._normalize(v)) - for k, v in value.items() - ) + value = super(_Container, self).normalize(value) + return self.type(self._apply('_normalize', element) for element in self._iter(value)) def validate(self, value): """ - Validate the given dictionary. + Validate the given container. + + Each element in the container will be validated with the specified field + instances. + """ + super(_Container, self).validate(value) + for element in self._iter(value): + self._apply('_validate', element) + + +class Dict(_Container): + """ + This field represents the built-in `dict` type. + + Each key and value will be serialized, deserialized, normalized, and + validated with the specified key and value types. The key and value types + can be specified using `Field` classes, `Field` instances, `~serde.Model` + classes, or built-in types that have a corresponding field type in this + library. + + Args: + key: the `Field` class or instance for keys in this `Dict`. + value: the `Field` class or instance for values in this `Dict`. + **kwargs: keyword arguments for the `Field` constructor. + """ + + def __init__(self, key=None, value=None, **kwargs): + """ + Create a new `Dict`. + """ + super(Dict, self).__init__(dict, **kwargs) + self.key = _resolve_to_field_instance(key) + self.value = _resolve_to_field_instance(value) - Each key and value in the dictionary will be validated with the - specified key and value field instances. + def _iter(self, value): + """ + Iterate over the dictionary items. """ - super(Dict, self).validate(value) + return value.items() - for k, v in value.items(): - self.key._validate(k) - self.value._validate(v) + 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)) class OrderedDict(Dict): @@ -807,10 +828,16 @@ class OrderedDict(Dict): **kwargs: keyword arguments for the `Field` constructor. """ - type = collections.OrderedDict + def __init__(self, key=None, value=None, **kwargs): + """ + Create a new `OrderedDict`. + """ + super(Dict, self).__init__(collections.OrderedDict, **kwargs) + self.key = _resolve_to_field_instance(key) + self.value = _resolve_to_field_instance(value) -class List(Instance): +class List(_Container): """ This field represents the built-in `list` type. @@ -831,50 +858,8 @@ def __init__(self, element=None, **kwargs): super(List, self).__init__(list, **kwargs) self.element = _resolve_to_field_instance(element) - def serialize(self, value): - """ - Serialize the given list. - Each element in the list will be serialized with the specified element - field instance. - """ - value = [self.element._serialize(v) for v in value] - return super(List, self).serialize(value) - - def deserialize(self, value): - """ - Deserialize the given list. - - Each element in the list will be deserialized with the specified element - field instance. - """ - value = super(List, self).deserialize(value) - return [self.element._deserialize(v) for v in value] - - def normalize(self, value): - """ - Normalize the given list. - - Each element in the list will be normalized with the specified element - field instance. - """ - value = super(List, self).normalize(value) - return [self.element._normalize(v) for v in value] - - def validate(self, value): - """ - Validate the given list. - - Each element in the list will be validated with the specified element - field instance. - """ - super(List, self).validate(value) - - for v in value: - self.element._validate(v) - - -class Set(Instance): +class Set(_Container): """ This field represents the built-in `set` type. @@ -895,50 +880,8 @@ def __init__(self, element=None, **kwargs): super(Set, self).__init__(set, **kwargs) self.element = _resolve_to_field_instance(element) - def serialize(self, value): - """ - Serialize the given set. - - Each element in the list will be serialized with the specified - element field instance. - """ - value = {self.element._serialize(v) for v in value} - return super(Set, self).serialize(value) - - def deserialize(self, value): - """ - Deserialize the given set. - Each element in the set will be deserialized with the specified - element field instance. - """ - value = super(Set, self).deserialize(value) - return {self.element._deserialize(v) for v in value} - - def normalize(self, value): - """ - Normalize the given set. - - Each element in the set will be normalized with the specified - element field instance. - """ - value = super(Set, self).normalize(value) - return {self.element._normalize(v) for v in value} - - def validate(self, value): - """ - Validate the given set. - - Each element in the set will be validated with the specified - element field instance. - """ - super(Set, self).validate(value) - - for v in value: - self.element._validate(v) - - -class Tuple(Instance): +class Tuple(_Container): """ This field represents the built-in `tuple` type. @@ -962,55 +905,18 @@ def __init__(self, *elements, **kwargs): for e in elements ) - def serialize(self, value): - """ - Serialize the given tuple. - - Each element in the tuple will be serialized with the specified element - Field instance. - """ - return tuple( - e._serialize(v) - for e, v in zip_equal(self.elements, value) - ) - - def deserialize(self, value): - """ - Deserialize the given tuple. - - Each element in the tuple will be deserialized with the specified - element field instance. - """ - value = super(Tuple, self).deserialize(value) - return tuple( - e._deserialize(v) - for e, v in zip_equal(self.elements, value) - ) - - def normalize(self, value): + def _iter(self, value): """ - Normalize the given tuple. - - Each element in the tuple will be normalized with the specified element - field instance. + Iterate over the fields and each element in the tuple. """ - value = super(Tuple, self).normalize(value) - return tuple( - e._normalize(v) - for e, v in zip_equal(self.elements, value) - ) + return zip_equal(self.elements, value) - def validate(self, value): + def _apply(self, stage, element): """ - Validate the given tuple. - - Each element in the tuple will be validated with the specified element - field instance. + Apply the element field stage to the corresponding element value. """ - super(Tuple, self).validate(value) - - for e, v in zip_equal(self.elements, value): - e._validate(v) + f, v = element + return getattr(f, stage)(v) def create_primitive(name, type): diff --git a/tests/test_fields.py b/tests/test_fields.py index d71a92e..8cdd739 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,7 +1,7 @@ +import collections import datetime import re import uuid -from collections import OrderedDict from pytest import raises @@ -32,6 +32,7 @@ Literal, Nested, Optional, + OrderedDict, Regex, Set, Str, @@ -669,7 +670,7 @@ class Example(Model): a = Field() field = Nested(Example) - assert field.serialize(Example(a=0)) == OrderedDict([('a', 0)]) + assert field.serialize(Example(a=0)) == collections.OrderedDict([('a', 0)]) def test_deserialize(self): # A Nested should deserialize as a dictionary representation of the @@ -781,6 +782,24 @@ def test_validate_extra(self): field.validate({'test': 11}) +class TestOrderedDict: + + def test___init___basic(self): + # Construct a basic OrderedDict and check values are set correctly. + field = OrderedDict() + assert field.key == Field() + assert field.value == Field() + assert field.validators == [] + + def test___init___options(self): + # Construct a OrderedDict with extra options and make sure values are + # passed to Field. + field = OrderedDict(key=Str, value=Int, validators=[None]) + assert field.key == Str() + assert field.value == Int() + assert field.validators == [None] + + class TestList: def test___init___basic(self):