Repository: swistakm/graceful

Issue:
Add "write_only" fields

Discussion:

Patch:
diff --git a/docs/guide/serializers.rst b/docs/guide/serializers.rst
index 722da38..bb9f64b 100644
--- a/docs/guide/serializers.rst
+++ b/docs/guide/serializers.rst
@@ -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::
 
diff --git a/src/graceful/fields.py b/src/graceful/fields.py
index b306aa2..d7fbf89 100644
--- a/src/graceful/fields.py
+++ b/src/graceful/fields.py
@@ -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:
 
@@ -77,6 +82,7 @@ class BaseField:
             validators=None,
             many=False,
             read_only=False,
+            write_only=False,
     ):
         """Initialize field definition."""
         self.label = label
@@ -85,6 +91,12 @@ class BaseField:
         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.
@@ -142,6 +154,7 @@ class BaseField:
             '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
diff --git a/src/graceful/serializers.py b/src/graceful/serializers.py
index e83a76a..6a914d0 100644
--- a/src/graceful/serializers.py
+++ b/src/graceful/serializers.py
@@ -118,6 +118,9 @@ class BaseSerializer(metaclass=MetaSerializer):
         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)


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 888fb87..4d30e78 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -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"
@@ -33,7 +42,8 @@ def test_base_field_describe():
         'details': "bar",
         'type': "anything",
         'spec': None,
-        'read_only': False
+        'read_only': False,
+        'write_only': False
     }
 
     # test extending descriptions by call with kwargs
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 5e7b353..0a10553 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -5,6 +5,8 @@ we test how whole framework for defining new serializers works.
 """
 import pytest
 
+import graceful
+from graceful.errors import DeserializationError
 from graceful.fields import BaseField, StringField
 from graceful.serializers import BaseSerializer
 
@@ -197,6 +199,56 @@ def test_serializer_get_attribute():
     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