Skip to content

Commit

Permalink
Merge pull request #53 from swistakm/feature/write-only-fields
Browse files Browse the repository at this point in the history
Add write_only option to base field. Fixes #45.
  • Loading branch information
swistakm authored Apr 14, 2017
2 parents d7679ac + edb7463 commit ab1a9cf
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 5 deletions.
7 changes: 6 additions & 1 deletion docs/guide/serializers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,14 @@ All field classes accept this set of arguments:

* **validators** *(list, optional):* list of validator callables.

* **many** *(bool, optional)* set to True if field is in fact a list
* **many** *(bool, optional):* set to True if field is in fact a list
of given type objects

* **read_only** *(bool):* True if field is read-only and cannot be set/modified
via POST, PUT, or PATCH requests.

* **write_only** *(bool):* True if field is write-only and cannot be retrieved
via GET requests.

.. note::

Expand Down
19 changes: 16 additions & 3 deletions src/graceful/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ class BaseField:
validators (list): list of validator callables.
many (bool): set to True if field is in fact a list of given type
objects
objects.
read_only (bool): True if field is read only and cannot be set/modified
by POST and PUT requests
read_only (bool): True if field is read-only and cannot be set/modified
via POST, PUT, or PATCH requests.
write_only (bool): True if field is write-only and cannot be retrieved
via GET requests.
.. versionadded:: 0.5.0
Example:
Expand Down Expand Up @@ -77,6 +82,7 @@ def __init__(
validators=None,
many=False,
read_only=False,
write_only=False,
):
"""Initialize field definition."""
self.label = label
Expand All @@ -85,6 +91,12 @@ def __init__(
self.validators = validators or []
self.many = many
self.read_only = read_only
self.write_only = write_only

if self.write_only and self.read_only:
raise ValueError(
"Field cannot be read-only and write-only at the same time."
)

def from_representation(self, data):
"""Convert representation value to internal value.
Expand Down Expand Up @@ -142,6 +154,7 @@ def description(self, **kwargs):
'type': "list of {}".format(self.type) if self.many else self.type,
'spec': self.spec,
'read_only': self.read_only,
'write_only': self.write_only,
}
description.update(kwargs)
return description
Expand Down
3 changes: 3 additions & 0 deletions src/graceful/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ def to_representation(self, obj):
representation = {}

for name, field in self.fields.items():
if field.write_only:
continue

# note fields do not know their names in source representation
# but may know what attribute they target from source object
attribute = self.get_attribute(obj, field.source or name)
Expand Down
12 changes: 11 additions & 1 deletion tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ def test_base_field_implementation_hooks():
field.from_representation(None)


def test_base_field_write_or_read_only_not_both():

assert BaseField("Read only", read_only=True).write_only is False
assert BaseField("Write only", write_only=True).read_only is False

with pytest.raises(ValueError):
BaseField("Both", read_only=True, write_only=True)


def test_base_field_describe():
class SomeField(BaseField):
type = "anything"
Expand All @@ -33,7 +42,8 @@ class SomeField(BaseField):
'details': "bar",
'type': "anything",
'spec': None,
'read_only': False
'read_only': False,
'write_only': False
}

# test extending descriptions by call with kwargs
Expand Down
52 changes: 52 additions & 0 deletions tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""
import pytest

import graceful
from graceful.errors import DeserializationError
from graceful.fields import BaseField, StringField
from graceful.serializers import BaseSerializer

Expand Down Expand Up @@ -197,6 +199,56 @@ def __init__(self):
assert serializer.get_attribute(instance, 'nonexistens') is None


def test_serializer_read_only_write_only_serialization():
class ExampleSerializer(BaseSerializer):
readonly = ExampleField('A read-only field', read_only=True)
writeonly = ExampleField('A write-only field', write_only=True)

serializer = ExampleSerializer()

assert serializer.to_representation(
{"writeonly": "foo", 'readonly': 'bar'}
) == {"readonly": "bar"}


@pytest.mark.xfail(
# note: this is forward compatibility test that will ensure the behaviour
# will change in future major release.
graceful.VERSION[0] < 1,
reason="graceful<1.0.0 does not enforce validation on deserialization",
strict=True,
)
def test_serializer_read_only_write_only_deserialization():
class ExampleSerializer(BaseSerializer):
readonly = ExampleField('A read-only field', read_only=True)
writeonly = ExampleField('A write-only field', write_only=True)

serializer = ExampleSerializer()

serializer.from_representation({"writeonly": "x"}) == {"writeonly": "x"}

with pytest.raises(DeserializationError):
serializer.from_representation({"writeonly": "x", 'readonly': 'x'})

with pytest.raises(DeserializationError):
serializer.from_representation({'readonly': 'x'})


def test_serializer_read_only_write_only_validation():
class ExampleSerializer(BaseSerializer):
readonly = ExampleField('A read-only field', read_only=True)
writeonly = ExampleField('A write-only field', write_only=True)

serializer = ExampleSerializer()
serializer.validate({"writeonly": "x"})

with pytest.raises(DeserializationError):
serializer.validate({"writeonly": "x", 'readonly': 'x'})

with pytest.raises(DeserializationError):
serializer.validate({'readonly': 'x'})


def test_serializer_source_wildcard():
"""
Test that '*' wildcard causes whole instance is returned on get attribute
Expand Down

0 comments on commit ab1a9cf

Please sign in to comment.