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

Commit

Permalink
Add Deque field
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Dec 1, 2019
1 parent bb06330 commit ec44671
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 5 deletions.
4 changes: 2 additions & 2 deletions Justfile
Expand Up @@ -41,13 +41,13 @@ install-all: check-venv

# Run all lints.
lint:
$VIRTUAL_ENV/bin/black --skip-string-normalization --check .
$VIRTUAL_ENV/bin/black --target-version py27 --skip-string-normalization --check .
$VIRTUAL_ENV/bin/flake8 --max-complexity 10 .

# Blacken and sort import statements
blacken:
$VIRTUAL_ENV/bin/isort --recursive .
$VIRTUAL_ENV/bin/black --skip-string-normalization .
$VIRTUAL_ENV/bin/black --target-version py27 --skip-string-normalization .

# Run tests excluding doctests.
test:
Expand Down
1 change: 1 addition & 0 deletions RELEASES.rst
Expand Up @@ -6,6 +6,7 @@ Releases

*Unreleased*

- Add ``Deque`` field.
- Add ``default`` keyword argument to ``Field``. (`#111`_)
- Fix bug in ``Uuid.normalize()``.
- Rename ``Constant`` field to ``Literal``. (`#118`_)
Expand Down
58 changes: 55 additions & 3 deletions src/serde/fields.py
Expand Up @@ -739,6 +739,13 @@ class _Container(Instance):
A base class for `Dict`, `List`, `Tuple`, and other container fields.
"""

def __init__(self, *args, **kwargs):
"""
Create a new `_Container`.
"""
super(_Container, self).__init__(*args, **kwargs)
self.kwargs = {}

def _iter(self, value):
"""
Iterate over the container.
Expand All @@ -759,7 +766,8 @@ def serialize(self, value):
field instances.
"""
value = self.type(
self._apply('_serialize', element) for element in self._iter(value)
(self._apply('_serialize', element) for element in self._iter(value)),
**self.kwargs
)
return super(_Container, self).serialize(value)

Expand All @@ -772,7 +780,8 @@ def deserialize(self, value):
"""
value = super(_Container, self).deserialize(value)
return self.type(
self._apply('_deserialize', element) for element in self._iter(value)
(self._apply('_deserialize', element) for element in self._iter(value)),
**self.kwargs
)

def normalize(self, value):
Expand All @@ -784,7 +793,8 @@ def normalize(self, value):
"""
value = super(_Container, self).normalize(value)
return self.type(
self._apply('_normalize', element) for element in self._iter(value)
(self._apply('_normalize', element) for element in self._iter(value)),
**self.kwargs
)

def validate(self, value):
Expand All @@ -799,6 +809,47 @@ def validate(self, value):
self._apply('_validate', element)


class Deque(_Container):
"""
A `~collections.deque` field.
Each element is serialized, deserialized, normalized and validated with the
specified element type. The element type 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:
element: the `Field` class or instance for elements in the `Deque`.
maxlen (int): the maximum length of this `Deque`.
**kwargs: keyword arguments for the `Field` constructor.
"""

def __init__(self, element=None, maxlen=None, **kwargs):
"""
Create a new `Deque`.
"""
super(Deque, self).__init__(collections.deque, **kwargs)
self.element = _resolve_to_field_instance(element)
self.kwargs = {'maxlen': maxlen}

@property
def maxlen(self):
"""
The maximum length of this `Deque`.
"""
return self.kwargs['maxlen']

def validate(self, value):
"""
Validate the given deque.
"""
super(Deque, self).validate(value)
if value.maxlen != self.maxlen:
raise ValidationError(
'expected max length of {} but got {}'.format(self.maxlen, value.maxlen)
)


class Dict(_Container):
"""
This field represents the built-in `dict` type.
Expand Down Expand Up @@ -1248,6 +1299,7 @@ def normalize(self, value):
str: Str,
tuple: Tuple,
# Collections
collections.deque: Deque,
collections.OrderedDict: OrderedDict,
# Datetimes
datetime.datetime: DateTime,
Expand Down
69 changes: 69 additions & 0 deletions tests/test_fields.py
Expand Up @@ -2,6 +2,7 @@
import datetime
import re
import uuid
from collections import deque

from pytest import raises

Expand All @@ -23,6 +24,7 @@
Constant,
Date,
DateTime,
Deque,
Dict,
Field,
Float,
Expand Down Expand Up @@ -783,6 +785,73 @@ def test___init__(self):
assert field.validators == []


class TestDeque:
def test___init__(self):
# Construct a basic Deque and check values are set correctly.
field = Deque()
assert field.element == Field()
assert field.kwargs == {'maxlen': None}
assert field.validators == []

def test___init___options(self):
# Construct a Deque with extra options and make sure values are passed
# to Field.
field = Deque(element=Str, maxlen=5, validators=[None])
assert field.element == Str()
assert field.kwargs == {'maxlen': 5}
assert field.validators == [None]

def test_serialize(self):
# A Deque should serialize values based on the element Field.
field = Deque(element=Reversed, maxlen=1)
assert field.serialize(deque(['test', 'hello'])) == deque(['olleh'])

def test_serialize_extra(self):
# A Deque should serialize values based on the element Field.
field = Deque(element=Field(serializers=[lambda x: x[::-1]]))
assert field.serialize(deque(['test', 'hello'], maxlen=1)) == deque(
['olleh'], maxlen=1
)

def test_deserialize(self):
# A Deque should deserialize values based on the element Field.
field = Deque(element=Reversed, maxlen=1)
assert field.deserialize(deque(['tset', 'olleh'])) == deque(['hello'], maxlen=1)

def test_deserialize_extra(self):
# A Deque should deserialize values based on the element Field.
field = Deque(element=Field(deserializers=[lambda x: x[::-1]]), maxlen=1)
assert field.deserialize(deque(['tset', 'olleh'])) == deque(['hello'], maxlen=1)

def test_normalize(self):
# A Deque should normalize values based on the element Field.
field = Deque(element=Field, maxlen=1)
assert field.normalize(deque(['test', 'hello'])) == deque(['hello'], maxlen=1)

def test_normalize_extra(self):
# A Deque should normalize values based on the element Field.
field = Deque(element=Field(normalizers=[lambda x: x[::-1]]), maxlen=1)
assert field.normalize(deque(['tset', 'olleh'])) == deque(['hello'], maxlen=1)

def test_validate(self):
# A Deque should validate values based on the element Field.
field = Deque(element=Int, maxlen=3)
field.validate(deque([0, 1, 2, 3, 4], maxlen=3))

with raises(ValidationError):
field.validate(deque(['1', '2', 'a', 'string']))
with raises(ValidationError):
field.validate(deque([0, 1], maxlen=2))

def test_validate_extra(self):
# A Deque should validate values based on the element Field.
field = Deque(element=Field(validators=[validators.Between(10, 10)]), maxlen=4)
field.validate(deque([10, 10, 10], maxlen=4))

with raises(ValidationError):
field.validate(deque([10, 11, 12, 13], maxlen=4))


class TestDict:
def test___init___basic(self):
# Construct a basic Dict and check values are set correctly.
Expand Down

0 comments on commit ec44671

Please sign in to comment.