Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for deserialization #2

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change these back to .data

# {'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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to .data

# {'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
125 changes: 103 additions & 22 deletions serpy/fields.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,99 @@
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
more control is needed, override :meth:`Field.as_getter`.
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
as ``operator.attrgetter``. If this is not supplied, the name this
field was assigned to on the serializer will be used.
: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 \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i prefer to use parens instead of \ to break lines:

self.read_only = (read_only or call or
  (attr is not None and '.' in attr))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

(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

Override this method to clean and validate values deserialized by this
field. For example to implement an ``int`` field: ::

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):
def to_internal_value(self, data):
return data

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you still want to cast it back to int?


: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 +105,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 +118,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 +183,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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would rather not specify the default to getattr and have these methods fail if method_name doesn't exist

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


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