From c2843d026c728daa81cfee7866185c5c77da8189 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Dec 2019 10:51:27 +0200 Subject: [PATCH] Only generate ModelSerializer fields once --- docs/api-guide/serializers.md | 16 ++-- rest_framework/serializers.py | 160 +++++++++++++++++++--------------- 2 files changed, 100 insertions(+), 76 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4679b1ed1..d4d8177b2 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -587,7 +587,7 @@ For full details see the [serializer relations][relations] documentation. ## Customizing field mappings -The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer. +The ModelSerializer class also exposes a set of class attributes that you can override in order to alter how serializer fields are automatically determined when first instantiating the serializer. Normally if a `ModelSerializer` does not generate the fields you need by default then you should either add them to the class explicitly, or simply use a regular `Serializer` class instead. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model. @@ -617,15 +617,15 @@ Defaults to `serializers.ChoiceField` ### The field_class and field_kwargs API -The following methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of `(field_class, field_kwargs)`. +The following class methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of `(field_class, field_kwargs)`. -### `.build_standard_field(self, field_name, model_field)` +### `.build_standard_field(cls, field_name, model_field)` Called to generate a serializer field that maps to a standard model field. The default implementation returns a serializer class based on the `serializer_field_mapping` attribute. -### `.build_relational_field(self, field_name, relation_info)` +### `.build_relational_field(cls, field_name, relation_info)` Called to generate a serializer field that maps to a relational model field. @@ -633,7 +633,7 @@ The default implementation returns a serializer class based on the `serializer_r The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. -### `.build_nested_field(self, field_name, relation_info, nested_depth)` +### `.build_nested_field(cls, field_name, relation_info, nested_depth)` Called to generate a serializer field that maps to a relational model field, when the `depth` option has been set. @@ -643,17 +643,17 @@ The `nested_depth` will be the value of the `depth` option, minus one. The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties. -### `.build_property_field(self, field_name, model_class)` +### `.build_property_field(cls, field_name, model_class)` Called to generate a serializer field that maps to a property or zero-argument method on the model class. The default implementation returns a `ReadOnlyField` class. -### `.build_url_field(self, field_name, model_class)` +### `.build_url_field(cls, field_name, model_class)` Called to generate a serializer field for the serializer's own `url` field. The default implementation returns a `HyperlinkedIdentityField` class. -### `.build_unknown_field(self, field_name, model_class)` +### `.build_unknown_field(cls, field_name, model_class)` Called when the field name did not map to any model field or model property. The default implementation raises an error, although subclasses may customize this behavior. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 18f4d0df6..c1b4f0290 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1002,32 +1002,29 @@ class ModelSerializer(Serializer): # Determine the fields to apply... - def get_fields(self): - """ - Return the dict of field names -> field instances that should be - used for `self.fields` when instantiating the serializer. - """ - if self.url_field_name is None: - self.url_field_name = api_settings.URL_FIELD_NAME + @classmethod + def _get_fields(cls): + if cls.url_field_name is None: + cls.url_field_name = api_settings.URL_FIELD_NAME - assert hasattr(self, 'Meta'), ( + assert hasattr(cls, 'Meta'), ( 'Class {serializer_class} missing "Meta" attribute'.format( - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) - assert hasattr(self.Meta, 'model'), ( + assert hasattr(cls.Meta, 'model'), ( 'Class {serializer_class} missing "Meta.model" attribute'.format( - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) - if model_meta.is_abstract_model(self.Meta.model): + if model_meta.is_abstract_model(cls.Meta.model): raise ValueError( 'Cannot use ModelSerializer with Abstract Models.' ) - declared_fields = copy.deepcopy(self._declared_fields) - model = getattr(self.Meta, 'model') - depth = getattr(self.Meta, 'depth', 0) + declared_fields = cls._declared_fields + model = getattr(cls.Meta, 'model') + depth = getattr(cls.Meta, 'depth', 0) if depth is not None: assert depth >= 0, "'depth' may not be negative." @@ -1035,12 +1032,12 @@ class ModelSerializer(Serializer): # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) - field_names = self.get_field_names(declared_fields, info) + field_names = cls.get_field_names(declared_fields, info) # Determine any extra field arguments and hidden fields that # should be included - extra_kwargs = self.get_extra_kwargs() - extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs( + extra_kwargs = cls.get_extra_kwargs() + extra_kwargs, hidden_fields = cls.get_uniqueness_extra_kwargs( field_names, declared_fields, extra_kwargs ) @@ -1059,12 +1056,12 @@ class ModelSerializer(Serializer): source = field_name # Determine the serializer field class and keyword arguments. - field_class, field_kwargs = self.build_field( + field_class, field_kwargs = cls.build_field( source, info, model, depth ) # Include any kwargs defined in `Meta.extra_kwargs` - field_kwargs = self.include_extra_kwargs( + field_kwargs = cls.include_extra_kwargs( field_kwargs, extra_field_kwargs ) @@ -1076,17 +1073,30 @@ class ModelSerializer(Serializer): return fields + def get_fields(self): + """ + Return the dict of field names -> field instances that should be + used for `self.fields` when instantiating the serializer. + """ + cls = self.__class__ + # We don't want the cache to traverse the MRO, since each subclass + # has its own fields; hence the cls comparison. + if not hasattr(cls, "_fields_cache") or cls._fields_cache[0] != cls: + cls._fields_cache = cls, cls._get_fields() + return copy.deepcopy(cls._fields_cache[1]) + # Methods for determining the set of field names to include... - def get_field_names(self, declared_fields, info): + @classmethod + def get_field_names(cls, declared_fields, info): """ Returns the list of all field names that should be created when instantiating this serializer class. This is based on the default set of fields, but also takes into account the `Meta.fields` or `Meta.exclude` options if they have been specified. """ - fields = getattr(self.Meta, 'fields', None) - exclude = getattr(self.Meta, 'exclude', None) + fields = getattr(cls.Meta, 'fields', None) + exclude = getattr(cls.Meta, 'exclude', None) if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)): raise TypeError( @@ -1103,7 +1113,7 @@ class ModelSerializer(Serializer): assert not (fields and exclude), ( "Cannot set both 'fields' and 'exclude' options on " "serializer {serializer_class}.".format( - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) @@ -1112,7 +1122,7 @@ class ModelSerializer(Serializer): "or the 'exclude' attribute has been deprecated since 3.3.0, " "and is now disallowed. Add an explicit fields = '__all__' to the " "{serializer_class} serializer.".format( - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ), ) @@ -1127,8 +1137,8 @@ class ModelSerializer(Serializer): # in order to allow serializer subclasses to only include # a subset of fields. required_field_names = set(declared_fields) - for cls in self.__class__.__bases__: - required_field_names -= set(getattr(cls, '_declared_fields', [])) + for basecls in cls.__bases__: + required_field_names -= set(getattr(basecls, '_declared_fields', [])) for field_name in required_field_names: assert field_name in fields, ( @@ -1136,25 +1146,25 @@ class ModelSerializer(Serializer): "{serializer_class}, but has not been included in the " "'fields' option.".format( field_name=field_name, - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) return fields # Use the default set of field names if `Meta.fields` is not specified. - fields = self.get_default_field_names(declared_fields, info) + fields = cls.get_default_field_names(declared_fields, info) if exclude is not None: # If `Meta.exclude` is included, then remove those fields. for field_name in exclude: - assert field_name not in self._declared_fields, ( + assert field_name not in cls._declared_fields, ( "Cannot both declare the field '{field_name}' and include " "it in the {serializer_class} 'exclude' option. Remove the " "field or, if inherited from a parent serializer, disable " "with `{field_name} = None`." .format( field_name=field_name, - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) @@ -1163,14 +1173,15 @@ class ModelSerializer(Serializer): "{serializer_class} in the 'exclude' option, but does " "not match any model field.".format( field_name=field_name, - serializer_class=self.__class__.__name__ + serializer_class=cls.__name__ ) ) fields.remove(field_name) return fields - def get_default_field_names(self, declared_fields, model_info): + @classmethod + def get_default_field_names(cls, declared_fields, model_info): """ Return the default list of field names that will be used if the `Meta.fields` option is not specified. @@ -1184,47 +1195,49 @@ class ModelSerializer(Serializer): # Methods for constructing serializer fields... - def build_field(self, field_name, info, model_class, nested_depth): + @classmethod + def build_field(cls, field_name, info, model_class, nested_depth): """ Return a two tuple of (cls, kwargs) to build a serializer field with. """ if field_name in info.fields_and_pk: model_field = info.fields_and_pk[field_name] - return self.build_standard_field(field_name, model_field) + return cls.build_standard_field(field_name, model_field) elif field_name in info.relations: relation_info = info.relations[field_name] if not nested_depth: - return self.build_relational_field(field_name, relation_info) + return cls.build_relational_field(field_name, relation_info) else: - return self.build_nested_field(field_name, relation_info, nested_depth) + return cls.build_nested_field(field_name, relation_info, nested_depth) elif hasattr(model_class, field_name): - return self.build_property_field(field_name, model_class) + return cls.build_property_field(field_name, model_class) - elif field_name == self.url_field_name: - return self.build_url_field(field_name, model_class) + elif field_name == cls.url_field_name: + return cls.build_url_field(field_name, model_class) - return self.build_unknown_field(field_name, model_class) + return cls.build_unknown_field(field_name, model_class) - def build_standard_field(self, field_name, model_field): + @classmethod + def build_standard_field(cls, field_name, model_field): """ Create regular model fields. """ - field_mapping = ClassLookupDict(self.serializer_field_mapping) + field_mapping = ClassLookupDict(cls.serializer_field_mapping) field_class = field_mapping[model_field] field_kwargs = get_field_kwargs(field_name, model_field) # Special case to handle when a OneToOneField is also the primary key if model_field.one_to_one and model_field.primary_key: - field_class = self.serializer_related_field + field_class = cls.serializer_related_field field_kwargs['queryset'] = model_field.related_model.objects if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. - field_class = self.serializer_choice_field + field_class = cls.serializer_choice_field # Some model fields may introduce kwargs that would not be valid # for the choice field. We need to strip these out. # Eg. models.DecimalField(max_digits=3, decimal_places=1, choices=DECIMAL_CHOICES) @@ -1258,24 +1271,25 @@ class ModelSerializer(Serializer): # Populate the `child` argument on `ListField` instances generated # for the PostgreSQL specific `ArrayField`. child_model_field = model_field.base_field - child_field_class, child_field_kwargs = self.build_standard_field( + child_field_class, child_field_kwargs = cls.build_standard_field( 'child', child_model_field ) field_kwargs['child'] = child_field_class(**child_field_kwargs) return field_class, field_kwargs - def build_relational_field(self, field_name, relation_info): + @classmethod + def build_relational_field(cls, field_name, relation_info): """ Create fields for forward and reverse relationships. """ - field_class = self.serializer_related_field + field_class = cls.serializer_related_field field_kwargs = get_relation_kwargs(field_name, relation_info) to_field = field_kwargs.pop('to_field', None) if to_field and not relation_info.reverse and not relation_info.related_model._meta.get_field(to_field).primary_key: field_kwargs['slug_field'] = to_field - field_class = self.serializer_related_to_field + field_class = cls.serializer_related_to_field # `view_name` is only valid for hyperlinked relationships. if not issubclass(field_class, HyperlinkedRelatedField): @@ -1283,7 +1297,8 @@ class ModelSerializer(Serializer): return field_class, field_kwargs - def build_nested_field(self, field_name, relation_info, nested_depth): + @classmethod + def build_nested_field(cls, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """ @@ -1298,7 +1313,8 @@ class ModelSerializer(Serializer): return field_class, field_kwargs - def build_property_field(self, field_name, model_class): + @classmethod + def build_property_field(cls, field_name, model_class): """ Create a read only field for model methods and properties. """ @@ -1307,16 +1323,18 @@ class ModelSerializer(Serializer): return field_class, field_kwargs - def build_url_field(self, field_name, model_class): + @classmethod + def build_url_field(cls, field_name, model_class): """ Create a field representing the object's own URL. """ - field_class = self.serializer_url_field + field_class = cls.serializer_url_field field_kwargs = get_url_kwargs(model_class) return field_class, field_kwargs - def build_unknown_field(self, field_name, model_class): + @classmethod + def build_unknown_field(cls, field_name, model_class): """ Raise an error on any unknown fields. """ @@ -1325,6 +1343,7 @@ class ModelSerializer(Serializer): (field_name, model_class.__name__) ) + @classmethod def include_extra_kwargs(self, kwargs, extra_kwargs): """ Include any 'extra_kwargs' that have been included for this field, @@ -1350,14 +1369,15 @@ class ModelSerializer(Serializer): # Methods for determining additional keyword arguments to apply... - def get_extra_kwargs(self): + @classmethod + def get_extra_kwargs(cls): """ Return a dictionary mapping field names to a dictionary of additional keyword arguments. """ - extra_kwargs = copy.deepcopy(getattr(self.Meta, 'extra_kwargs', {})) + extra_kwargs = copy.deepcopy(getattr(cls.Meta, 'extra_kwargs', {})) - read_only_fields = getattr(self.Meta, 'read_only_fields', None) + read_only_fields = getattr(cls.Meta, 'read_only_fields', None) if read_only_fields is not None: if not isinstance(read_only_fields, (list, tuple)): raise TypeError( @@ -1372,15 +1392,16 @@ class ModelSerializer(Serializer): else: # Guard against the possible misspelling `readonly_fields` (used # by the Django admin and others). - assert not hasattr(self.Meta, 'readonly_fields'), ( + assert not hasattr(cls.Meta, 'readonly_fields'), ( 'Serializer `%s.%s` has field `readonly_fields`; ' 'the correct spelling for the option is `read_only_fields`.' % - (self.__class__.__module__, self.__class__.__name__) + (cls.__module__, cls.__name__) ) return extra_kwargs - def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs): + @classmethod + def get_uniqueness_extra_kwargs(cls, field_names, declared_fields, extra_kwargs): """ Return any additional field options that need to be included as a result of uniqueness constraints on the model. This is returned as @@ -1388,11 +1409,11 @@ class ModelSerializer(Serializer): ('dict of updated extra kwargs', 'mapping of hidden fields') """ - if getattr(self.Meta, 'validators', None) is not None: + if getattr(cls.Meta, 'validators', None) is not None: return (extra_kwargs, {}) - model = getattr(self.Meta, 'model') - model_fields = self._get_model_fields( + model = getattr(cls.Meta, 'model') + model_fields = cls._get_model_fields( field_names, declared_fields, extra_kwargs ) @@ -1454,14 +1475,15 @@ class ModelSerializer(Serializer): return extra_kwargs, hidden_fields - def _get_model_fields(self, field_names, declared_fields, extra_kwargs): + @classmethod + def _get_model_fields(cls, field_names, declared_fields, extra_kwargs): """ Returns all the model fields that are being mapped to by fields on the serializer class. Returned as a dict of 'model field name' -> 'model field'. Used internally by `get_uniqueness_field_options`. """ - model = getattr(self.Meta, 'model') + model = getattr(cls.Meta, 'model') model_fields = {} for field_name in field_names: @@ -1608,19 +1630,21 @@ class HyperlinkedModelSerializer(ModelSerializer): """ serializer_related_field = HyperlinkedRelatedField - def get_default_field_names(self, declared_fields, model_info): + @classmethod + def get_default_field_names(cls, declared_fields, model_info): """ Return the default list of field names that will be used if the `Meta.fields` option is not specified. """ return ( - [self.url_field_name] + + [cls.url_field_name] + list(declared_fields) + list(model_info.fields) + list(model_info.forward_relations) ) - def build_nested_field(self, field_name, relation_info, nested_depth): + @classmethod + def build_nested_field(cls, field_name, relation_info, nested_depth): """ Create nested fields for forward and reverse relationships. """