diff options
| author | Tom Christie | 2011-06-14 18:22:13 +0100 |
|---|---|---|
| committer | Tom Christie | 2011-06-14 18:22:13 +0100 |
| commit | 412b5fc2d54def2f2601b860b80afaa52d595e58 (patch) | |
| tree | b700e41c2d17a52d4726da7f62027c50f391b5e9 /djangorestframework/serializer.py | |
| parent | 323d52e7c44c208dce545a8084f7401384fd731e (diff) | |
| download | django-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.py | 347 |
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) |
