diff --git a/README.rst b/README.rst index 9b37f76..cf43e8e 100644 --- a/README.rst +++ b/README.rst @@ -79,11 +79,11 @@ Simple Example z = serpy.Field() f = Foo(1) - FooSerializer(f).data + FooSerializer(instance=f).data # {'x': 1, 'y': 'hello', 'z': 9.5} fs = [Foo(i) for i in range(100)] - FooSerializer(fs, many=True).data + FooSerializer(instance=fs, many=True).data # [{'x': 0, 'y': 'hello', 'z': 9.5}, {'x': 1, 'y': 'hello', 'z': 9.5}, ...] Nested Example @@ -112,7 +112,7 @@ Nested Example nested = NesteeSerializer() f = Foo() - FooSerializer(f).data + FooSerializer(instance=f).data # {'x': 1, 'nested': {'n': 'hi'}} Complex Example @@ -139,7 +139,7 @@ Complex Example return obj.y + obj.z f = Foo() - FooSerializer(f).data + FooSerializer(instance=f).data # {'w': 10, 'x': 5, 'plus': 3} Inheritance Example @@ -165,9 +165,9 @@ Inheritance Example b = serpy.Field() f = Foo() - ASerializer(f).data + ASerializer(instance=f).data # {'a': 1} - ABSerializer(f).data + ABSerializer(instance=f).data # {'a': 1, 'b': 2} License diff --git a/docs/custom-fields.rst b/docs/custom-fields.rst index c65466d..40df80a 100644 --- a/docs/custom-fields.rst +++ b/docs/custom-fields.rst @@ -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: @@ -25,7 +25,7 @@ Then to use it: f = Obj() f.foo = 9 - ObjSerializer(f).data + ObjSerializer(instance=f).data # {'foo': 14} Another use for custom fields is data validation. For example, to validate that @@ -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 diff --git a/serpy/fields.py b/serpy/fields.py index efc73fc..e67a10f 100644 --- a/serpy/fields.py +++ b/serpy/fields.py @@ -1,5 +1,6 @@ import six import types +import warnings class Field(object): @@ -7,8 +8,8 @@ class Field(object): 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 @@ -16,38 +17,83 @@ 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 + + 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 + + :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. @@ -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 @@ -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): @@ -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(instance=foo).data # {'plus': 15, 'minus': -5} :param str method: The method on the serializer to call. Defaults to ``'get_'``. """ 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) + + 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) diff --git a/serpy/serializer.py b/serpy/serializer.py index f2b63af..580a64f 100644 --- a/serpy/serializer.py +++ b/serpy/serializer.py @@ -1,43 +1,81 @@ -from serpy.fields import Field import operator import six +from serpy.fields import Field + class SerializerBase(Field): - _field_map = {} + pass -def _compile_field_to_tuple(field, name, serializer_cls): +def _compile_read_field_to_tuple(field, name, serializer_cls): getter = field.as_getter(name, serializer_cls) if getter is None: - getter = serializer_cls.default_getter(field.attr or name) + getter = serializer_cls._meta.default_getter(field.attr or name) - # Only set a to_value function if it has been overriden for performance. - to_value = None - if field._is_to_value_overriden(): - to_value = field.to_value + # Only set a to_representation function if it has been overridden + # for performance. + to_representation = None + if field._is_to_representation_overridden(): + to_representation = field.to_representation - return (name, getter, to_value, field.call, field.required, + return (name, getter, to_representation, field.call, field.required, field.getter_takes_serializer) +def _compile_write_field_to_tuple(field, name, serializer_cls): + setter = field.as_setter(name, serializer_cls) + if setter is None: + setter = serializer_cls._meta.default_setter(field.attr or name) + + # Only set a to_internal_value function if it has been overridden + # for performance. + to_internal_value = None + if field._is_to_internal_value_overridden(): + to_internal_value = field.to_internal_value + + return (name, setter, to_internal_value, field.call, field.required, + field.setter_takes_serializer) + + class SerializerMeta(type): @staticmethod - def _get_fields(direct_fields, serializer_cls): + def _compile_meta(direct_fields, serializer_meta, serializer_cls): field_map = {} + meta_bases = () # Get all the fields from base classes. - for cls in serializer_cls.__mro__[::-1]: - if issubclass(cls, SerializerBase): - field_map.update(cls._field_map) + for cls in serializer_cls.__bases__[::-1]: + if issubclass(cls, SerializerBase) and cls is not SerializerBase: + field_map.update(cls._meta._field_map) + meta_bases = meta_bases + (type(cls._meta),) field_map.update(direct_fields) + if serializer_meta: + meta_bases = meta_bases + (serializer_meta,) - compiled_fields = [ - _compile_field_to_tuple(field, name, serializer_cls) + # get the right order of meta bases + meta_bases = meta_bases[::-1] + + compiled_read_fields = [ + _compile_read_field_to_tuple(field, name, serializer_cls) + for name, field in field_map.items() + ] + + compiled_write_fields = [ + _compile_write_field_to_tuple(field, name, serializer_cls) for name, field in field_map.items() - ] + if not field.read_only + ] + + # automatically create an inner-class Meta that inherits from + # parent class's inner-class Meta + Meta = type('Meta', meta_bases, {}) + meta = Meta() + meta._field_map = field_map + meta._compiled_read_fields = compiled_read_fields + meta._compiled_write_fields = compiled_write_fields - return field_map, compiled_fields + return meta def __new__(cls, name, bases, attrs): # Fields declared directly on the class. @@ -50,15 +88,31 @@ def __new__(cls, name, bases, attrs): for k in direct_fields.keys(): del attrs[k] + serializer_meta = attrs.pop('Meta', None) + real_cls = super(SerializerMeta, cls).__new__(cls, name, bases, attrs) - field_map, compiled_fields = cls._get_fields(direct_fields, real_cls) + real_cls._meta = cls._compile_meta( + direct_fields, serializer_meta, real_cls + ) - real_cls._field_map = field_map - real_cls._compiled_fields = tuple(compiled_fields) return real_cls +@staticmethod +def attrsetter(attr_name): + """ + attrsetter(attr) --> attrsetter object + + Return a callable object that sets the given attribute(s) on its first + operand as the second operand + After f = attrsetter('name'), the call f(o, val) executes: o.name = val + """ + def _attrsetter(obj, val): + setattr(obj, attr_name, val) + return _attrsetter + + class Serializer(six.with_metaclass(SerializerMeta, SerializerBase)): """:class:`Serializer` is used as a base for custom serializers. @@ -74,25 +128,39 @@ class FooSerializer(Serializer): bar = Field() foo = Foo(foo='hello', bar=5) - FooSerializer(foo).data + FooSerializer(instance=foo).data # {'foo': 'hello', 'bar': 5} - :param obj: The object or objects to serialize. + A particular Serializer object can either serialize or deserialize, but + not both. + + :param instance: The object or objects to serialize. + :param data: The data to deserialize. + :param klass: The class for instantiating the deserialized object :param bool many: If ``obj`` is a collection of objects, set ``many`` to ``True`` to serialize to a list. """ - #: The default getter used if :meth:`Field.as_getter` returns None. - default_getter = operator.attrgetter + # Inner-class + class Meta(object): + cls = None + default_getter = operator.attrgetter + default_setter = attrsetter - def __init__(self, obj=None, many=False, **kwargs): + def __init__(self, instance=None, data=None, many=False, **kwargs): super(Serializer, self).__init__(**kwargs) - self.obj = obj + self._can_serialize = instance is not None + self._can_deserialize = not self._can_serialize and data is not None + if self._can_serialize: + self._initial_instance = instance + self._data = None + elif self._can_deserialize: + self._initial_data = data + self._instance = None self.many = many - self._data = None def _serialize(self, obj, fields): v = {} - for name, getter, to_value, call, required, pass_self in fields: + for name, getter, to_repr, call, required, pass_self in fields: if pass_self: result = getter(self, obj) else: @@ -100,30 +168,64 @@ def _serialize(self, obj, fields): if required or result is not None: if call: result = result() - if to_value: - result = to_value(result) + if to_repr: + result = to_repr(result) v[name] = result return v - def to_value(self, obj): - fields = self._compiled_fields + def _deserialize(self, data, fields): + v = self._meta.cls() + for name, setter, to_internal, call, required, pass_self in fields: + if pass_self: + setter(self, v, data[name]) + else: + if required: + value = data[name] + else: + value = data.get(name) + if to_internal and (required or value is not None): + value = to_internal(value) + setter(v, value) + return v + + def to_representation(self, obj): + fields = self._meta._compiled_read_fields if self.many: serialize = self._serialize return [serialize(o, fields) for o in obj] return self._serialize(obj, fields) + def to_internal_value(self, data): + fields = self._meta._compiled_write_fields + if self.many: + deserialize = self._deserialize + return [deserialize(o, fields) for o in data] + return self._deserialize(data, fields) + @property def data(self): """Get the serialized data from the :class:`Serializer`. - The data will be cached for future accesses. + The return value will be cached for future accesses. """ # Cache the data for next time .data is called. if self._data is None: - self._data = self.to_value(self.obj) + self._data = self.to_representation(self._initial_instance) return self._data + @property + def deserialized_value(self): + """Get the deserialized value from the :class:`Serializer`. + + The return value will be cached for future accesses. + """ + # Cache the deserialized_value for next time .deserialized_value is + # called. + if self._instance is None: + self._instance = self.to_internal_value(self._initial_data) + return self._instance + class DictSerializer(Serializer): """:class:`DictSerializer` serializes python ``dicts`` instead of objects. @@ -139,7 +241,8 @@ class FooSerializer(DictSerializer): bar = FloatField() foo = {'foo': '5', 'bar': '2.2'} - FooSerializer(foo).data + FooSerializer(instance=foo).data # {'foo': 5, 'bar': 2.2} """ - default_getter = operator.itemgetter + class Meta: + default_getter = operator.itemgetter diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 diff --git a/tests/test_fields.py b/tests/test_fields.py index ea84a2f..25b7dbb 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,71 +1,139 @@ -from .obj import Obj +import unittest +import warnings + from serpy.fields import ( Field, MethodField, BoolField, IntField, FloatField, StrField) -import unittest +from tests.obj import Obj class TestFields(unittest.TestCase): def test_to_value_noop(self): - self.assertEqual(Field().to_value(5), 5) - self.assertEqual(Field().to_value('a'), 'a') - self.assertEqual(Field().to_value(None), None) + self.assertEqual(Field().to_representation(5), 5) + self.assertEqual(Field().to_representation('a'), 'a') + self.assertEqual(Field().to_representation(None), None) + + def test_to_internal_value_noop(self): + self.assertEqual(Field().to_internal_value(5), 5) + self.assertEqual(Field().to_internal_value('a'), 'a') + self.assertEqual(Field().to_internal_value(None), None) def test_as_getter_none(self): self.assertEqual(Field().as_getter(None, None), None) - def test_is_to_value_overriden(self): + def test_as_setter_none(self): + self.assertEqual(Field().as_setter(None, None), None) + + def test_is_to_representation_overridden(self): class TransField(Field): - def to_value(self, value): + def to_representation(self, value): return value field = Field() - self.assertFalse(field._is_to_value_overriden()) + self.assertFalse(field._is_to_representation_overridden()) field = TransField() - self.assertTrue(field._is_to_value_overriden()) + self.assertTrue(field._is_to_representation_overridden()) field = IntField() - self.assertTrue(field._is_to_value_overriden()) + self.assertTrue(field._is_to_representation_overridden()) + + def test_is_to_internal_value_overridden(self): + class TransField(Field): + def to_internal_value(self, value): + return value + + field = Field() + self.assertFalse(field._is_to_internal_value_overridden()) + field = TransField() + self.assertTrue(field._is_to_internal_value_overridden()) def test_str_field(self): field = StrField() - self.assertEqual(field.to_value('a'), 'a') - self.assertEqual(field.to_value(5), '5') + self.assertEqual(field.to_representation('a'), 'a') + self.assertEqual(field.to_representation(5), '5') + self.assertEqual(field.to_internal_value('a'), 'a') + self.assertEqual(field.to_internal_value(5), '5') def test_bool_field(self): field = BoolField() - self.assertTrue(field.to_value(True)) - self.assertFalse(field.to_value(False)) - self.assertTrue(field.to_value(1)) - self.assertFalse(field.to_value(0)) + self.assertTrue(field.to_representation(True)) + self.assertFalse(field.to_representation(False)) + self.assertTrue(field.to_representation(1)) + self.assertFalse(field.to_representation(0)) + self.assertTrue(field.to_internal_value(True)) + self.assertFalse(field.to_internal_value(False)) + self.assertTrue(field.to_internal_value(1)) + self.assertFalse(field.to_internal_value(0)) def test_int_field(self): field = IntField() - self.assertEqual(field.to_value(5), 5) - self.assertEqual(field.to_value(5.4), 5) - self.assertEqual(field.to_value('5'), 5) + self.assertEqual(field.to_representation(5), 5) + self.assertEqual(field.to_representation(5.4), 5) + self.assertEqual(field.to_representation('5'), 5) + self.assertEqual(field.to_internal_value(5), 5) + self.assertEqual(field.to_internal_value(5.4), 5) + self.assertEqual(field.to_internal_value('5'), 5) def test_float_field(self): field = FloatField() - self.assertEqual(field.to_value(5.2), 5.2) - self.assertEqual(field.to_value('5.5'), 5.5) + self.assertEqual(field.to_representation(5.2), 5.2) + self.assertEqual(field.to_representation('5.5'), 5.5) + self.assertEqual(field.to_internal_value(5.2), 5.2) + self.assertEqual(field.to_internal_value('5.5'), 5.5) def test_method_field(self): class FakeSerializer(object): def get_a(self, obj): return obj.a + def set_a(self, obj, value): + obj.a = value + def z_sub_1(self, obj): return obj.z - 1 + def z_add_1(self, obj, value): + obj.z = value + 1 + serializer = FakeSerializer() - fn = MethodField().as_getter('a', serializer) + field = MethodField() + fn = field.as_getter('a', serializer) self.assertEqual(fn(Obj(a=3)), 3) - fn = MethodField('z_sub_1').as_getter('a', serializer) + fn = field.as_setter('a', serializer) + o = Obj(a=-1) + fn(o, 3) + self.assertEqual(o.a, 3) + + field = MethodField('z_sub_1', 'z_add_1') + fn = field.as_getter('z', serializer) self.assertEqual(fn(Obj(z=3)), 2) + fn = field.as_setter('z', serializer) + o = Obj(a=-1) + fn(o, 2) + self.assertEqual(o.z, 3) + self.assertTrue(MethodField.getter_takes_serializer) + self.assertTrue(MethodField.setter_takes_serializer) + + def test_to_value_backwards_compatibility(self): + class AddOneIntField(IntField): + def to_value(self, value): + return super(AddOneIntField, self).to_value(value) + 1 + + self.assertEqual(AddOneIntField().to_value('1'), 2) + + self.assertEqual(IntField().to_value('1'), 1) + + def test_to_value_deprecation_warning(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + IntField().to_value('1') + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) + self.assertTrue('deprecated' in str(w[-1].message)) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 384ab94..b1f3e22 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,34 +1,56 @@ -from .obj import Obj +import unittest + from serpy.fields import Field, MethodField, IntField, FloatField, StrField from serpy.serializer import Serializer, DictSerializer -import unittest +from tests.obj import Obj class TestSerializer(unittest.TestCase): def test_simple(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() a = Obj(a=5) - self.assertEqual(ASerializer(a).data['a'], 5) + self.assertEqual(ASerializer(instance=a).data['a'], 5) - def test_data_cached(self): + a = ASerializer(data={'a': 5}).deserialized_value + self.assertEqual(a.a, 5) + + def test_data_and_obj_cached(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() a = Obj(a=5) - serializer = ASerializer(a) + serializer = ASerializer(instance=a) data1 = serializer.data data2 = serializer.data # Use assertTrue instead of assertIs for python 2.6. self.assertTrue(data1 is data2) + serializer = ASerializer(data={'a': 5}) + obj1 = serializer.deserialized_value + obj2 = serializer.deserialized_value + # Use assertTrue instead of assertIs for python 2.6. + self.assertTrue(obj1 is obj2) + def test_inheritance(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() class CSerializer(Serializer): + class Meta: + cls = Obj + c = Field() class ABSerializer(ASerializer): @@ -38,17 +60,31 @@ class ABCSerializer(ABSerializer, CSerializer): pass a = Obj(a=5, b='hello', c=100) - self.assertEqual(ASerializer(a).data['a'], 5) - data = ABSerializer(a).data + self.assertEqual(ASerializer(instance=a).data['a'], 5) + data = ABSerializer(instance=a).data self.assertEqual(data['a'], 5) self.assertEqual(data['b'], 'hello') - data = ABCSerializer(a).data + data = ABCSerializer(instance=a).data self.assertEqual(data['a'], 5) self.assertEqual(data['b'], 'hello') self.assertEqual(data['c'], 100) + a = {'a': 5, 'b': 'hello', 'c': 100} + serializer = ASerializer(data=a) + self.assertEqual(serializer.deserialized_value.a, 5) + serializer = ABSerializer(data=a) + self.assertEqual(serializer.deserialized_value.a, 5) + self.assertEqual(serializer.deserialized_value.b, 'hello') + serializer = ABCSerializer(data=a) + self.assertEqual(serializer.deserialized_value.a, 5) + self.assertEqual(serializer.deserialized_value.b, 'hello') + self.assertEqual(serializer.deserialized_value.c, 100) + def test_many(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() objs = [Obj(a=i) for i in range(5)] @@ -60,115 +96,276 @@ class ASerializer(Serializer): self.assertEqual(data[3]['a'], 3) self.assertEqual(data[4]['a'], 4) + data = [{'a': 0}, {'a': 1}, {'a': 2}, {'a': 3}, {'a': 4}] + objs = ASerializer(data=data, many=True).deserialized_value + self.assertEqual(len(objs), 5) + self.assertEqual(objs[0].a, 0) + self.assertEqual(objs[1].a, 1) + self.assertEqual(objs[2].a, 2) + self.assertEqual(objs[3].a, 3) + self.assertEqual(objs[4].a, 4) + def test_serializer_as_field(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() class BSerializer(Serializer): + class Meta: + cls = Obj + b = ASerializer() b = Obj(b=Obj(a=3)) - self.assertEqual(BSerializer(b).data['b']['a'], 3) + self.assertEqual(BSerializer(instance=b).data['b']['a'], 3) + + data = {'b': {'a': 3}} + obj = BSerializer(data=data).deserialized_value + self.assertEqual(obj.b.a, 3) def test_serializer_as_field_many(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() class BSerializer(Serializer): + class Meta: + cls = Obj + b = ASerializer(many=True) b = Obj(b=[Obj(a=i) for i in range(3)]) - b_data = BSerializer(b).data['b'] + b_data = BSerializer(instance=b).data['b'] self.assertEqual(len(b_data), 3) self.assertEqual(b_data[0]['a'], 0) self.assertEqual(b_data[1]['a'], 1) self.assertEqual(b_data[2]['a'], 2) + data = {'b': [{'a': 0}, {'a': 1}, {'a': 2}]} + obj = BSerializer(data=data).deserialized_value + self.assertEqual(len(obj.b), 3) + self.assertEqual(obj.b[0].a, 0) + self.assertEqual(obj.b[1].a, 1) + self.assertEqual(obj.b[2].a, 2) + def test_serializer_as_field_call(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field() class BSerializer(Serializer): + class Meta: + cls = Obj + b = ASerializer(call=True) b = Obj(b=lambda: Obj(a=3)) - self.assertEqual(BSerializer(b).data['b']['a'], 3) + self.assertEqual(BSerializer(instance=b).data['b']['a'], 3) + data = {'b': {'a': 3}} + b = BSerializer(data=data).deserialized_value + self.assertFalse(hasattr(b, 'b')) def test_serializer_method_field(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = MethodField() - b = MethodField('add_9') + b = MethodField('add_9', 'sub_9') def get_a(self, obj): return obj.a + 5 + def set_a(self, obj, value): + obj.a = value - 5 + def add_9(self, obj): - return obj.a + 9 + return obj.b + 9 + + def sub_9(self, obj, value): + obj.b = value - 9 - a = Obj(a=2) - data = ASerializer(a).data + a = Obj(a=2, b=2) + data = ASerializer(instance=a).data self.assertEqual(data['a'], 7) self.assertEqual(data['b'], 11) + data = {'a': 7, 'b': 11} + obj = ASerializer(data=data).deserialized_value + self.assertEqual(obj.a, 2) + self.assertEqual(obj.b, 2) - def test_to_value_called(self): + def test_field_called(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = IntField() b = FloatField(call=True) c = StrField(attr='foo.bar.baz') o = Obj(a='5', b=lambda: '6.2', foo=Obj(bar=Obj(baz=10))) - data = ASerializer(o).data + data = ASerializer(instance=o).data self.assertEqual(data['a'], 5) self.assertEqual(data['b'], 6.2) self.assertEqual(data['c'], '10') + data = {'a': 5, 'b': 6.2, 'c': '10'} + obj = ASerializer(data=data).deserialized_value + self.assertEqual(obj.a, 5) + self.assertFalse(hasattr(obj, 'b')) + self.assertFalse(hasattr(obj, 'foo')) def test_dict_serializer(self): class ASerializer(DictSerializer): + class Meta: + cls = Obj + a = IntField() b = Field(attr='foo') d = {'a': '2', 'foo': 'hello'} - data = ASerializer(d).data + data = ASerializer(instance=d).data self.assertEqual(data['a'], 2) self.assertEqual(data['b'], 'hello') + data = {'a': 2, 'b': 'hello'} + obj = ASerializer(data=data).deserialized_value + self.assertEqual(obj.a, 2) + self.assertEqual(obj.foo, 'hello') def test_dotted_attr(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = Field('a.b.c') o = Obj(a=Obj(b=Obj(c=2))) - data = ASerializer(o).data + data = ASerializer(instance=o).data self.assertEqual(data['a'], 2) + data = {'a': 2} + obj = ASerializer(data=data).deserialized_value + self.assertFalse(hasattr(obj, 'a')) def test_custom_field(self): class Add5Field(Field): - def to_value(self, value): + def to_representation(self, value): return value + 5 + def to_internal_value(self, data): + return data - 5 + class ASerializer(Serializer): + class Meta: + cls = Obj + a = Add5Field() o = Obj(a=10) - data = ASerializer(o).data + data = ASerializer(instance=o).data self.assertEqual(data['a'], 15) + data = {'a': 15} + obj = ASerializer(data=data).deserialized_value + self.assertEqual(obj.a, 10) def test_optional_field(self): class ASerializer(Serializer): + class Meta: + cls = Obj + a = IntField(required=False) o = Obj(a=None) - data = ASerializer(o).data - self.assertEqual(data['a'], None) + data = ASerializer(instance=o).data + self.assertTrue(data['a'] is None) + + data = {'a': None} + obj = ASerializer(data=data).deserialized_value + self.assertTrue(obj.a is None) o = Obj(a='5') - data = ASerializer(o).data + data = ASerializer(instance=o).data self.assertEqual(data['a'], 5) + data = {'a': 5} + obj = ASerializer(data=data).deserialized_value + self.assertEqual(obj.a, 5) + class ASerializer(Serializer): + class Meta: + cls = Obj + a = IntField() o = Obj(a=None) - self.assertRaises(TypeError, lambda: ASerializer(o).data) + self.assertRaises(TypeError, lambda: ASerializer(instance=o).data) + + data = {} + self.assertRaises( + KeyError, + lambda: ASerializer(data=data).deserialized_value + ) + + def test_read_only_field(self): + class ASerializer(Serializer): + class Meta: + cls = Obj + + a = IntField(read_only=True) + + o = Obj(a='5') + data = ASerializer(instance=o).data + self.assertEqual(data['a'], 5) + + data = {'a': 5} + obj = ASerializer(data=data).deserialized_value + self.assertFalse(hasattr(obj, 'a')) + + def test_serialization_requires_instance(self): + class ASerializer(Serializer): + class Meta: + cls = Obj + + a = IntField() + + self.assertRaises(AttributeError, lambda: ASerializer().data) + + def test_deserialization_requires_data(self): + class ASerializer(Serializer): + class Meta: + cls = Obj + + a = IntField() + + self.assertRaises( + AttributeError, + lambda: ASerializer().deserialized_value + ) + + def test_deserialization_requires_cls(self): + class ASerializer(Serializer): + a = IntField() + + data = {'a': 5} + self.assertRaises(TypeError, + lambda: ASerializer(data=data).deserialized_value) + + def test_can_only_do_serialization_or_deserialization(self): + class ASerializer(Serializer): + class Meta: + cls = Obj + + a = IntField() + + o = Obj(a='5') + data = {'a': 5} + serializer = ASerializer(instance=o, data=data) + self.assertRaises(AttributeError, + lambda: serializer.deserialized_value) if __name__ == '__main__':