diff --git a/CHANGELOG.md b/CHANGELOG.md index 3206cfa8..d9eaf504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/), any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. +## [Unreleased] + +### Changed + +* Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class. + +### Deprecated + +* Deprecated `get_included_serializers(serializer)` function under `rest_framework_json_api.utils`. Use `serializer.included_serializers` instead. + ## [4.2.1] - 2021-07-06 ### Fixed @@ -40,7 +50,6 @@ any parts of the framework not mentioned in the documentation should generally b * Deprecated default `format_type` argument of `rest_framework_json_api.utils.format_value`. Use `rest_framework_json_api.utils.format_field_name` or specify specifc `format_type` instead. * Deprecated `format_type` argument of `rest_framework_json_api.utils.format_link_segment`. Use `rest_framework_json_api.utils.format_value` instead. - ## [4.1.0] - 2021-03-08 ### Added diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index c9dd765d..605f7c1c 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -16,7 +16,6 @@ from rest_framework_json_api.utils import ( Hyperlink, format_link_segment, - get_included_serializers, get_resource_type_from_instance, get_resource_type_from_queryset, get_resource_type_from_serializer, @@ -274,7 +273,7 @@ def get_resource_type_from_included_serializer(self): inflection.singularize(field_name), inflection.pluralize(field_name), ] - includes = get_included_serializers(parent) + includes = getattr(parent, "included_serializers", dict()) for field in field_names: if field in includes.keys(): return get_resource_type_from_serializer(includes[field]) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b50c2b1a..3c649331 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -284,7 +284,9 @@ def extract_included( current_serializer = fields.serializer context = current_serializer.context - included_serializers = utils.get_included_serializers(current_serializer) + included_serializers = getattr( + current_serializer, "included_serializers", dict() + ) included_resources = copy.copy(included_resources) included_resources = [ inflection.underscore(value) for value in included_resources @@ -692,8 +694,8 @@ def _get_included_serializers(cls, serializer, prefix="", already_seen=None): included_serializers = [] already_seen.add(serializer) - for include, included_serializer in utils.get_included_serializers( - serializer + for include, included_serializer in getattr( + serializer, "included_serializers", dict() ).items(): included_serializers.append(f"{prefix}{include}") included_serializers.extend( diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py index 77a0d4ed..efe14914 100644 --- a/rest_framework_json_api/schemas/openapi.py +++ b/rest_framework_json_api/schemas/openapi.py @@ -1,7 +1,6 @@ import warnings from urllib.parse import urljoin -from django.utils.module_loading import import_string as import_class_from_dotted_path from rest_framework.fields import empty from rest_framework.relations import ManyRelatedField from rest_framework.schemas import openapi as drf_openapi @@ -379,13 +378,7 @@ def _find_related_view(self, view_endpoints, related_serializer, parent_view): """ for path, method, view in view_endpoints: view_serializer = view.get_serializer() - if not isinstance(related_serializer, type): - related_serializer_class = import_class_from_dotted_path( - related_serializer - ) - else: - related_serializer_class = related_serializer - if isinstance(view_serializer, related_serializer_class): + if isinstance(view_serializer, related_serializer): return view return None diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index a73a5d47..bc60f193 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,8 +1,10 @@ from collections import OrderedDict +from collections.abc import Mapping import inflection from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet +from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ParseError @@ -22,7 +24,6 @@ from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import ( get_included_resources, - get_included_serializers, get_resource_type_from_instance, get_resource_type_from_model, get_resource_type_from_serializer, @@ -120,7 +121,7 @@ def __init__(self, *args, **kwargs): view = context.get("view") if context else None def validate_path(serializer_class, field_path, path): - serializers = get_included_serializers(serializer_class) + serializers = getattr(serializer_class, "included_serializers", None) if serializers is None: raise ParseError("This endpoint does not support the include parameter") this_field_name = inflection.underscore(field_path[0]) @@ -152,8 +153,55 @@ def validate_path(serializer_class, field_path, path): super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs) +class LazySerializersDict(Mapping): + """ + A dictionary of serializers which lazily import dotted class path and self. + """ + + def __init__(self, parent, serializers): + self.parent = parent + self.serializers = serializers + + def __getitem__(self, key): + value = self.serializers[key] + if not isinstance(value, type): + if value == "self": + value = self.parent + else: + value = import_class_from_dotted_path(value) + self.serializers[key] = value + + return value + + def __iter__(self): + return iter(self.serializers) + + def __len__(self): + return len(self.serializers) + + def __repr__(self): + return dict.__repr__(self.serializers) + + class SerializerMetaclass(SerializerMetaclass): - pass + def __new__(cls, name, bases, attrs): + serializer = super().__new__(cls, name, bases, attrs) + + if attrs.get("included_serializers", None): + setattr( + serializer, + "included_serializers", + LazySerializersDict(serializer, attrs["included_serializers"]), + ) + + if attrs.get("related_serializers", None): + setattr( + serializer, + "related_serializers", + LazySerializersDict(serializer, attrs["related_serializers"]), + ) + + return serializer # If user imports serializer from here we can catch class definition and check diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ac31979a..19c72809 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -1,4 +1,3 @@ -import copy import inspect import operator import warnings @@ -13,7 +12,6 @@ ) from django.http import Http404 from django.utils import encoding -from django.utils.module_loading import import_string as import_class_from_dotted_path from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions from rest_framework.exceptions import APIException @@ -342,20 +340,14 @@ def get_default_included_resources_from_serializer(serializer): def get_included_serializers(serializer): - included_serializers = copy.copy( - getattr(serializer, "included_serializers", dict()) + warnings.warn( + DeprecationWarning( + "Using of `get_included_serializers(serializer)` function is deprecated." + "Use `serializer.included_serializers` instead." + ) ) - for name, value in iter(included_serializers.items()): - if not isinstance(value, type): - if value == "self": - included_serializers[name] = ( - serializer if isinstance(serializer, type) else serializer.__class__ - ) - else: - included_serializers[name] = import_class_from_dotted_path(value) - - return included_serializers + return getattr(serializer, "included_serializers", dict()) def get_relation_instance(resource_instance, source, serializer): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index bb6f09bb..6b739582 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -11,7 +11,6 @@ from django.db.models.manager import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch -from django.utils.module_loading import import_string as import_class_from_dotted_path from rest_framework import generics, viewsets from rest_framework.exceptions import MethodNotAllowed, NotFound from rest_framework.fields import get_attribute @@ -183,8 +182,6 @@ def get_related_serializer_class(self): False ), 'Either "included_serializers" or "related_serializers" should be configured' - if not isinstance(_class, type): - return import_class_from_dotted_path(_class) return _class return parent_serializer_class diff --git a/tests/test_serializers.py b/tests/test_serializers.py new file mode 100644 index 00000000..6726fa8c --- /dev/null +++ b/tests/test_serializers.py @@ -0,0 +1,35 @@ +from django.db import models + +from rest_framework_json_api import serializers +from tests.models import DJAModel, ManyToManyTarget +from tests.serializers import ManyToManyTargetSerializer + + +def test_get_included_serializers(): + class IncludedSerializersModel(DJAModel): + self = models.ForeignKey("self", on_delete=models.CASCADE) + target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE) + other_target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE) + + class Meta: + app_label = "tests" + + class IncludedSerializersSerializer(serializers.ModelSerializer): + included_serializers = { + "self": "self", + "target": ManyToManyTargetSerializer, + "other_target": "tests.serializers.ManyToManyTargetSerializer", + } + + class Meta: + model = IncludedSerializersModel + fields = ("self", "other_target", "target") + + included_serializers = IncludedSerializersSerializer.included_serializers + expected_included_serializers = { + "self": IncludedSerializersSerializer, + "target": ManyToManyTargetSerializer, + "other_target": ManyToManyTargetSerializer, + } + + assert included_serializers == expected_included_serializers diff --git a/tests/test_utils.py b/tests/test_utils.py index 00bf0836..43c12dcf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -346,7 +346,7 @@ class PlainRelatedResourceTypeSerializer(serializers.Serializer): def test_get_included_serializers(): - class IncludedSerializersModel(DJAModel): + class DeprecatedIncludedSerializersModel(DJAModel): self = models.ForeignKey("self", on_delete=models.CASCADE) target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE) other_target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE) @@ -354,7 +354,7 @@ class IncludedSerializersModel(DJAModel): class Meta: app_label = "tests" - class IncludedSerializersSerializer(serializers.ModelSerializer): + class DeprecatedIncludedSerializersSerializer(serializers.ModelSerializer): included_serializers = { "self": "self", "target": ManyToManyTargetSerializer, @@ -362,12 +362,16 @@ class IncludedSerializersSerializer(serializers.ModelSerializer): } class Meta: - model = IncludedSerializersModel + model = DeprecatedIncludedSerializersModel fields = ("self", "other_target", "target") - included_serializers = get_included_serializers(IncludedSerializersSerializer) + with pytest.deprecated_call(): + included_serializers = get_included_serializers( + DeprecatedIncludedSerializersSerializer + ) + expected_included_serializers = { - "self": IncludedSerializersSerializer, + "self": DeprecatedIncludedSerializersSerializer, "target": ManyToManyTargetSerializer, "other_target": ManyToManyTargetSerializer, }