aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework/serializer.py
diff options
context:
space:
mode:
authorTom Christie2011-06-14 18:22:13 +0100
committerTom Christie2011-06-14 18:22:13 +0100
commit412b5fc2d54def2f2601b860b80afaa52d595e58 (patch)
treeb700e41c2d17a52d4726da7f62027c50f391b5e9 /djangorestframework/serializer.py
parent323d52e7c44c208dce545a8084f7401384fd731e (diff)
downloaddjango-rest-framework-412b5fc2d54def2f2601b860b80afaa52d595e58.tar.bz2
Support for nesting resources etc...
--HG-- rename : djangorestframework/tests/resources.py => djangorestframework/tests/serializer.py
Diffstat (limited to 'djangorestframework/serializer.py')
-rw-r--r--djangorestframework/serializer.py347
1 files changed, 347 insertions, 0 deletions
diff --git a/djangorestframework/serializer.py b/djangorestframework/serializer.py
new file mode 100644
index 00000000..47c631bc
--- /dev/null
+++ b/djangorestframework/serializer.py
@@ -0,0 +1,347 @@
+"""
+Customizable serialization.
+"""
+from django.db import models
+from django.db.models.query import QuerySet
+from django.db.models.fields.related import RelatedField
+from django.utils.encoding import smart_unicode
+
+import decimal
+import inspect
+import types
+
+
+# We register serializer classes, so that we can refer to them by their
+# class names, if there are cyclical serialization heirachys.
+_serializers = {}
+
+
+def _field_to_tuple(field):
+ """
+ Convert an item in the `fields` attribute into a 2-tuple.
+ """
+ if isinstance(field, (tuple, list)):
+ return (field[0], field[1])
+ return (field, None)
+
+def _fields_to_list(fields):
+ """
+ Return a list of field names.
+ """
+ return [_field_to_tuple(field)[0] for field in fields or ()]
+
+def _fields_to_dict(fields):
+ """
+ Return a `dict` of field name -> None, or tuple of fields, or Serializer class
+ """
+ return dict([_field_to_tuple(field) for field in fields or ()])
+
+
+class _SkipField(Exception):
+ """
+ Signals that a serialized field should be ignored.
+ We use this mechanism as the default behavior for ensuring
+ that we don't infinitely recurse when dealing with nested data.
+ """
+ pass
+
+
+class _BuildInnerMeta(type):
+ """
+ Some magic so that an inner Meta class gets inheriting behavior.
+ """
+ def __new__(cls, name, bases, attrs):
+ # Get a list of all the inner Metas, from the bases upwards.
+ inner_metas = [getattr(base, 'Meta', object) for base in bases]
+ inner_metas.append(attrs.get('Meta', object))
+
+ # Build up the attributes on the inner Meta.
+ meta_attrs = {}
+ [meta_attrs.update(inner_meta.__dict__) for inner_meta in inner_metas]
+
+ # Drop private attributes.
+ meta_attrs = dict([ (key, val) for (key, val) in meta_attrs.items()
+ if not key.startswith('_') ])
+
+ # Lovely, now we can create our inner Meta class.
+ attrs['Meta'] = type('Meta', (object,), meta_attrs)
+
+ # Build the class and register it.
+ ret = super(_BuildInnerMeta, cls).__new__(cls, name, bases, attrs)
+ _serializers[name] = ret
+
+ return ret
+
+
+class Serializer(object):
+ """
+ Converts python objects into plain old native types suitable for
+ serialization. In particular it handles models and querysets.
+
+ The output format is specified by setting a number of attributes on the
+ inner `Meta` class.
+
+ You may also override any of the serialization methods, to provide
+ for more flexible behavior.
+
+ Valid output types include anything that may be directly rendered into
+ json, xml etc...
+ """
+ __metaclass__ = _BuildInnerMeta
+
+ class Meta:
+ """
+ Information on how to serialize the data.
+ """
+
+ fields = ()
+ """
+ Specify the fields to be serialized on a model or dict.
+ Overrides `include` and `exclude`.
+ """
+
+ include = ()
+ """
+ Fields to add to the default set to be serialized on a model/dict.
+ """
+
+ exclude = ()
+ """
+ Fields to remove from the default set to be serialized on a model/dict.
+ """
+
+ rename = {}
+ """
+ A dict of key->name to use for the field keys.
+ """
+
+ related_serializer = None
+ """
+ The default serializer class to use for any related models.
+ """
+
+ depth = None
+ """
+ The maximum depth to serialize to, or `None`.
+ """
+
+
+ def __init__(self, depth=None, stack=[], **kwargs):
+ """
+ Allow `Meta` items to be set on init.
+ """
+ self.depth = depth
+ self.stack = stack
+
+ if self.depth is None:
+ self.depth = self.Meta.depth
+
+ for (key, val) in kwargs.items():
+ setattr(self.Meta, key, val)
+
+
+ def get_fields(self, obj):
+ """
+ Return the set of field names/keys to use for a model instance/dict.
+ """
+ fields = self.Meta.fields
+
+ # If Meta.fields is not set, we use the default fields and modify
+ # them with Meta.include and Meta.exclude
+ if not fields:
+ default = self.get_default_fields(obj)
+ include = self.Meta.include or ()
+ exclude = self.Meta.exclude or ()
+ fields = set(default + list(include)) - set(exclude)
+
+ else:
+ fields = _fields_to_list(self.Meta.fields)
+
+ return fields
+
+
+ def get_default_fields(self, obj):
+ """
+ Return the default list of field names/keys for a model instance/dict.
+ These are used if `Meta.fields` is not given.
+ """
+ if isinstance(obj, models.Model):
+ opts = obj._meta
+ return [field.name for field in opts.fields + opts.many_to_many]
+ else:
+ return obj.keys()
+
+
+ def get_related_serializer(self, key):
+ info = _fields_to_dict(self.Meta.fields).get(key, None)
+
+ # If an element in `fields` is a 2-tuple of (str, tuple)
+ # then the second element of the tuple is the fields to
+ # set on the related serializer
+ if isinstance(info, (list, tuple)):
+ class OnTheFlySerializer(Serializer):
+ class Meta:
+ fields = info
+ return OnTheFlySerializer
+
+ # If an element in `fields` is a 2-tuple of (str, Serializer)
+ # then the second element of the tuple is the Serializer
+ # class to use for that field.
+ elif isinstance(info, type) and issubclass(info, Serializer):
+ return info
+
+ # If an element in `fields` is a 2-tuple of (str, str)
+ # then the second element of the tuple is the name of the Serializer
+ # class to use for that field.
+ #
+ # Black magic to deal with cyclical Serializer dependancies.
+ # Similar to what Django does for cyclically related models.
+ elif isinstance(info, str) and info in _serializers:
+ return _serializers[info]
+
+ # Otherwise use `related_serializer` or fall back to `Serializer`
+ return getattr(self.Meta, 'related_serializer') or Serializer
+
+
+ def serialize_key(self, key):
+ """
+ Keys serialize to their string value,
+ unless they exist in the `Meta.rename` dict.
+ """
+ return getattr(self.Meta.rename, key, key)
+
+
+ def serialize_val(self, key, obj):
+ """
+ Convert a model field or dict value into a serializable representation.
+ """
+ related_serializer = self.get_related_serializer(key)
+
+ if self.depth is None:
+ depth = None
+ elif self.depth <= 0:
+ return self.serialize_max_depth(obj)
+ else:
+ depth = self.depth - 1
+
+ if any([obj is elem for elem in self.stack]):
+ return self.serialize_recursion(obj)
+ else:
+ stack = self.stack[:]
+ stack.append(obj)
+
+ return related_serializer(depth=depth, stack=stack).serialize(obj)
+
+
+ def serialize_max_depth(self, obj):
+ """
+ Determine how objects should be serialized once `depth` is exceeded.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_recursion(self, obj):
+ """
+ Determine how objects should be serialized if recursion occurs.
+ The default behavior is to ignore the field.
+ """
+ raise _SkipField
+
+
+ def serialize_model(self, instance):
+ """
+ Given a model instance or dict, serialize it to a dict, using
+ the given behavior given on Meta.
+ """
+ data = {}
+
+ fields = self.get_fields(instance)
+
+ # serialize each required field
+ for fname in fields:
+ if hasattr(self, fname):
+ # check for a method 'fname' on self first
+ meth = getattr(self, fname)
+ if inspect.ismethod(meth) and len(inspect.getargspec(meth)[0]) == 2:
+ obj = meth(instance)
+ elif hasattr(instance, fname):
+ # now check for an attribute 'fname' on the instance
+ obj = getattr(instance, fname)
+ elif fname in instance:
+ # finally check for a key 'fname' on the instance
+ obj = instance[fname]
+ else:
+ continue
+
+ try:
+ key = self.serialize_key(fname)
+ val = self.serialize_val(fname, obj)
+ data[key] = val
+ except _SkipField:
+ pass
+
+ return data
+
+
+ def serialize_iter(self, obj):
+ """
+ Convert iterables into a serializable representation.
+ """
+ return [self.serialize(item) for item in obj]
+
+
+ def serialize_func(self, obj):
+ """
+ Convert no-arg methods and functions into a serializable representation.
+ """
+ return self.serialize(obj())
+
+
+ def serialize_manager(self, obj):
+ """
+ Convert a model manager into a serializable representation.
+ """
+ return self.serialize_iter(obj.all())
+
+
+ def serialize_decimal(self, obj):
+ """
+ Convert a Decimal instance into a serializable representation.
+ """
+ return str(obj)
+
+
+ def serialize_fallback(self, obj):
+ """
+ Convert any unhandled object into a serializable representation.
+ """
+ return smart_unicode(obj, strings_only=True)
+
+
+ def serialize(self, obj):
+ """
+ Convert any object into a serializable representation.
+ """
+
+ if isinstance(obj, (dict, models.Model)):
+ # Model instances & dictionaries
+ return self.serialize_model(obj)
+ elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
+ # basic iterables
+ return self.serialize_iter(obj)
+ elif isinstance(obj, models.Manager):
+ # Manager objects
+ return self.serialize_manager(obj)
+ elif isinstance(obj, decimal.Decimal):
+ # Decimals (force to string representation)
+ return self.serialize_decimal(obj)
+ elif inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
+ # function with no args
+ return self.serialize_func(obj)
+ elif inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
+ # bound method
+ return self.serialize_func(obj)
+
+ # fall back to smart unicode
+ return self.serialize_fallback(obj)