Skip to content

Commit

Permalink
Add support for deserialization
Browse files Browse the repository at this point in the history
- Added support for read-only fields
- Renamed `.data` to `.representation` (and added `.internal_value`) for better clarity
- Renamed `.to_value` to `.to_representation` (and added `to_internal_value`) for better clarity

TBD update documentation, fix benchmarks, update benchmarks

Fixes clarkduvall#1
  • Loading branch information
Aniruddha Maru committed Jun 3, 2015
1 parent 3663aaf commit 2273b24
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 111 deletions.
12 changes: 6 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ Simple Example
z = serpy.Field()
f = Foo(1)
FooSerializer(f).data
FooSerializer(f).representation
# {'x': 1, 'y': 'hello', 'z': 9.5}
fs = [Foo(i) for i in range(100)]
FooSerializer(fs, many=True).data
FooSerializer(fs, many=True).representation
# [{'x': 0, 'y': 'hello', 'z': 9.5}, {'x': 1, 'y': 'hello', 'z': 9.5}, ...]
Nested Example
Expand Down Expand Up @@ -112,7 +112,7 @@ Nested Example
nested = NesteeSerializer()
f = Foo()
FooSerializer(f).data
FooSerializer(f).representation
# {'x': 1, 'nested': {'n': 'hi'}}
Complex Example
Expand All @@ -139,7 +139,7 @@ Complex Example
return obj.y + obj.z
f = Foo()
FooSerializer(f).data
FooSerializer(f).representation
# {'w': 10, 'x': 5, 'plus': 3}
Inheritance Example
Expand All @@ -165,9 +165,9 @@ Inheritance Example
b = serpy.Field()
f = Foo()
ASerializer(f).data
ASerializer(f).representation
# {'a': 1}
ABSerializer(f).data
ABSerializer(f).representation
# {'a': 1, 'b': 2}
License
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def benchmark(serializer_fn, repetitions, num_objs=1, data=None):

t1 = time.time()
for i in range(repetitions):
serializer_fn(objs, many=many).data
serializer_fn(objs, many=many).representation
total_time = time.time() - t1
print('Total time: {}'.format(total_time))
print('Objs/s : {}\n'.format(int(total_objs / total_time)))
Expand Down
8 changes: 4 additions & 4 deletions docs/custom-fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ Custom Fields
*************

The most common way to create a custom field with **serpy** is to override
:meth:`serpy.Field.to_value`. This method is called on the value
:meth:`serpy.Field.to_representation`. This method is called on the value
retrieved from the object being serialized. For example, to create a field that
adds 5 to every value it serializes, do:

.. code-block:: python
class Add5Field(serpy.Field):
def to_value(self, value):
def to_representation(self, value):
return value + 5
Then to use it:
Expand All @@ -25,7 +25,7 @@ Then to use it:
f = Obj()
f.foo = 9
ObjSerializer(f).data
ObjSerializer(f).representation
# {'foo': 14}
Another use for custom fields is data validation. For example, to validate that
Expand All @@ -34,7 +34,7 @@ every serialized value has a ``'.'`` in it:
.. code-block:: python
class ValidateDotField(serpy.Field):
def to_value(self, value):
def to_representation(self, value):
if '.' not in value:
raise ValidationError('no dot!')
return value
Expand Down
116 changes: 95 additions & 21 deletions serpy/fields.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import six
import types
import warnings


class Field(object):
""":class:`Field` is used to define what attributes will be serialized.
A :class:`Field` maps a property or function on an object to a value in the
serialized result. Subclass this to make custom fields. For most simple
cases, overriding :meth:`Field.to_value` should give enough flexibility. If
cases, overriding :meth:`Field.to_representation` should give enough flexibility. If
more control is needed, override :meth:`Field.as_getter`.
:param str attr: The attribute to get on the object, using the same format
Expand All @@ -16,38 +17,76 @@ class Field(object):
:param bool call: Whether the value should be called after it is retrieved
from the object. Useful if an object has a method to be serialized.
:param bool required: Whether the field is required. If set to ``False``,
:meth:`Field.to_value` will not be called if the value is ``None``.
:meth:`Field.to_representation` will not be called if the value is ``None``.
:param bool read_only: Whether the field is read-only. If set to ``False``,
the field won't be deserialized. If ``call`` is True, or if ``attr``
contains a '.', then this param is set to True.
"""
#: Set to ``True`` if the value function returned from
#: :meth:`Field.as_getter` requires the serializer to be passed in as the
#: first argument. Otherwise, the object will be the only parameter.
getter_takes_serializer = False

def __init__(self, attr=None, call=False, required=True):
#: Set to ``True`` if the value function returned from
#: :meth:`Field.as_setter` requires the serializer to be passed in as the
#: first argument. Otherwise, the object will be the only parameter.
setter_takes_serializer = False

def __init__(self, attr=None, call=False, required=True, read_only=False):
self.attr = attr
self.call = call
self.required = required
self.read_only = read_only or call or (attr is not None and '.' in attr)

def to_value(self, value):
def to_representation(self, value):
"""Transform the serialized value.
Override this method to clean and validate values serialized by this
field. For example to implement an ``int`` field: ::
def to_value(self, value):
def to_representation(self, value):
return int(value)
:param value: The value fetched from the object being serialized.
"""
return value
to_value._serpy_base_implementation = True
to_representation._serpy_base_implementation = True

def _is_to_representation_overridden(self):
to_representation = self.to_representation
# If to_representation isn't a method, it must have been overridden.
if not isinstance(to_representation, types.MethodType):
return True
return not getattr(to_representation, '_serpy_base_implementation', False)

def to_value(self, obj):
warnings.warn(
".to_value method is deprecated, use .to_representation instead",
DeprecationWarning,
stacklevel=2
)
return self.to_representation(obj)

def to_internal_value(self, data):
"""Transform the serialized value into Python object
def _is_to_value_overriden(self):
to_value = self.to_value
# If to_value isn't a method, it must have been overriden.
if not isinstance(to_value, types.MethodType):
Override this method to clean and validate values deserialized by this
field. For example to implement an ``int`` field: ::
def to_internal_value(self, data):
return data
:param data: The data fetched from the object being deserialized.
"""
return data
to_internal_value._serpy_base_implementation = True

def _is_to_internal_value_overridden(self):
to_internal_value = self.to_internal_value
# If to_internal_value isn't a method, it must have been overridden.
if not isinstance(to_internal_value, types.MethodType):
return True
return not getattr(to_value, '_serpy_base_implementation', False)
return not getattr(to_internal_value, '_serpy_base_implementation', False)

def as_getter(self, serializer_field_name, serializer_cls):
"""Returns a function that fetches an attribute from an object.
Expand All @@ -59,7 +98,7 @@ def as_getter(self, serializer_field_name, serializer_cls):
converted into a getter function using this method. During
serialization, each getter will be called with the object being
serialized, and the return value will be passed through
:meth:`Field.to_value`.
:meth:`Field.to_representation`.
If a :class:`Field` has ``getter_takes_serializer = True``, then the
getter returned from this method will be called with the
Expand All @@ -72,25 +111,52 @@ def as_getter(self, serializer_field_name, serializer_cls):
"""
return None

def as_setter(self, serializer_field_name, serializer_cls):
"""Returns a function that sets an attribute on an object
Return ``None`` to use the default setter for the serializer defined in
:attr:`Serializer.default_setter`.
When a :class:`Serializer` is defined, each :class:`Field` will be
converted into a setter function using this method. During
deserialization, each setter will be called with the object being
deserialized with the argument passed as the return value of
:meth:`Field.to_internal_value`.
If a :class:`Field` has ``setter_takes_serializer = True``, then the
setter returned from this method will be called with the
:class:`Serializer` instance as the first argument, and the object
being serialized as the second.
:param str serializer_field_name: The name this field was assigned to
on the serializer.
:param serializer_cls: The :class:`Serializer` this field is a part of.
"""
return None


class StrField(Field):
"""A :class:`Field` that converts the value to a string."""
to_value = staticmethod(six.text_type)
to_representation = staticmethod(six.text_type)
to_internal_value = staticmethod(six.text_type)


class IntField(Field):
"""A :class:`Field` that converts the value to an integer."""
to_value = staticmethod(int)
to_representation = staticmethod(int)
to_internal_value = staticmethod(int)


class FloatField(Field):
"""A :class:`Field` that converts the value to a float."""
to_value = staticmethod(float)
to_representation = staticmethod(float)
to_internal_value = staticmethod(float)


class BoolField(Field):
"""A :class:`Field` that converts the value to a boolean."""
to_value = staticmethod(bool)
to_representation = staticmethod(bool)
to_internal_value = staticmethod(bool)


class MethodField(Field):
Expand All @@ -110,20 +176,28 @@ def do_minus(self, foo_obj):
return foo_obj.bar - foo_obj.baz
foo = Foo(bar=5, baz=10)
FooSerializer(foo).data
FooSerializer(foo).representation
# {'plus': 15, 'minus': -5}
:param str method: The method on the serializer to call. Defaults to
``'get_<field name>'``.
"""
getter_takes_serializer = True
setter_takes_serializer = True

def __init__(self, method=None, **kwargs):
def __init__(self, getter=None, setter=None, **kwargs):
super(MethodField, self).__init__(**kwargs)
self.method = method
self.getter_method = getter
self.setter_method = setter

def as_getter(self, serializer_field_name, serializer_cls):
method_name = self.method
method_name = self.getter_method
if method_name is None:
method_name = 'get_{0}'.format(serializer_field_name)
return getattr(serializer_cls, method_name)
return getattr(serializer_cls, method_name, None)

def as_setter(self, serializer_field_name, serializer_cls):
method_name = self.setter_method
if method_name is None:
method_name = 'set_{0}'.format(serializer_field_name)
return getattr(serializer_cls, method_name, None)
Loading

0 comments on commit 2273b24

Please sign in to comment.