From 0ab70ba1705ff0a80f0925167bb2f8c0c3155418 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Wed, 21 Aug 2013 16:44:20 +1200 Subject: [PATCH 1/2] Handle dotted field.source in writable serializers. --- rest_framework/fields.py | 59 ++++++++++++++++++++++++++++++----- rest_framework/relations.py | 8 +---- rest_framework/serializers.py | 3 +- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 07779c472..a0a59523c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -60,6 +60,18 @@ def get_component(obj, attr_name): return val +def set_component(obj, attr_name, value): + """ + Given an object, and an attribute name, set that attribute on the object. + Mirrors get_component, except set_component doesn't handle callable + components. + """ + if isinstance(obj, dict): + obj[attr_name] = value + else: + setattr(obj, attr_name, value) + + def readable_datetime_formats(formats): format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') @@ -138,6 +150,24 @@ class Field(object): if help_text is not None: self.help_text = strip_multiple_choice_msg(smart_text(help_text)) + def _get_source_value(self, obj, field_name): + """ + Given an object and a field name, traverses the components in + self.source/field_name and returns the source value from the object. + + The source/field_name may contain dot-separated components. + Each component should refer to an attribute, a dict key, or a + callable with no arguments. + """ + source = self.source or field_name + value = obj + + for component in source.split('.'): + value = get_component(value, component) + if value is None: + break + return value + def initialize(self, parent, field_name): """ Called to set up a field prior to field_to_native or field_from_native. @@ -170,13 +200,7 @@ class Field(object): if self.source == '*': return self.to_native(obj) - source = self.source or field_name - value = obj - - for component in source.split('.'): - value = get_component(value, component) - if value is None: - break + value = self._get_source_value(obj, field_name) return self.to_native(value) @@ -297,6 +321,27 @@ class WritableField(Field): if errors: raise ValidationError(errors) + def _set_source_value(self, obj, field_name, value): + """ + Looks up a field on the given object and sets its value. + Uses self.source if set, otherwise the given field name. + + This obeys the same rules as _get_source_value, except that the + final component of self.source/field_name can't be a callable. + """ + source = self.source or field_name + parts = source.split('.') + last_source_part = parts.pop() + + item = obj + for component in parts: + item = get_component(item, component) + if item is None: + raise ValueError( + "can't set %r: component %r is None" % (source, component) + ) + set_component(item, last_source_part, value) + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, diff --git a/rest_framework/relations.py b/rest_framework/relations.py index edaf76d6e..d5d90e49a 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -130,13 +130,7 @@ class RelatedField(WritableField): if self.source == '*': return self.to_native(obj) - source = self.source or field_name - value = obj - - for component in source.split('.'): - value = get_component(value, component) - if value is None: - break + value = self._get_source_value(obj, field_name) except ObjectDoesNotExist: return None diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa3447..c37a049e1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -289,7 +289,8 @@ class BaseSerializer(WritableField): are instantiated. """ if instance is not None: - instance.update(attrs) + for k, v in attrs.items(): + self._set_source_value(instance, k, v) return instance return attrs From d5d97624f702d0ae1aa68a8ecb5aa215f84c6316 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Wed, 21 Aug 2013 16:54:49 +1200 Subject: [PATCH 2/2] more explicit/obvious error if you try to do writable dotted field.source with a callable final component --- rest_framework/fields.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a0a59523c..21daa91b4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -69,6 +69,9 @@ def set_component(obj, attr_name, value): if isinstance(obj, dict): obj[attr_name] = value else: + attr = getattr(obj, attr_name) + if six.callable(attr): + raise TypeError("%r.%s is a method; can't set it" % (obj, attr_name)) setattr(obj, attr_name, value) @@ -336,10 +339,6 @@ class WritableField(Field): item = obj for component in parts: item = get_component(item, component) - if item is None: - raise ValueError( - "can't set %r: component %r is None" % (source, component) - ) set_component(item, last_source_part, value) def field_from_native(self, data, files, field_name, into):