Repository: pulp/pulpcore

Issue:
[task] Refactor linked serializer fields



Discussion:

Resolving patch:
diff --git a/pulpcore/app/serializers/base.py b/pulpcore/app/serializers/base.py
--- a/pulpcore/app/serializers/base.py
+++ b/pulpcore/app/serializers/base.py
@@ -26,6 +26,188 @@
 log = getLogger(__name__)
 
 
+# Field mixins
+
+
+class HrefFieldMixin:
+    """A mixin to configure related fields to generate relative hrefs."""
+
+    @property
+    def context(self):
+        # Removes the request from the context to display relative hrefs.
+        res = dict(super().context)
+        res["request"] = None
+        return res
+
+
+class _MatchingRegexViewName(object):
+    """This is a helper class to help defining object matching rules for master-detail.
+
+    If you can be specific, please specify the `view_name`, but if you cannot, this allows
+    you to specify a regular expression like .e.g. `r"repositories(-.*/.*)?-detail"` to
+    identify whether the provided resources view name belongs to any repository type.
+    """
+
+    __slots__ = ("pattern",)
+
+    def __init__(self, pattern):
+        self.pattern = pattern
+
+    def __repr__(self):
+        return f'{self.__class__.__name__}(r"{self.pattern}")'
+
+    def __eq__(self, other):
+        return re.fullmatch(self.pattern, other) is not None
+
+
+class _DetailFieldMixin(HrefFieldMixin):
+    """Mixin class containing code common to DetailIdentityField and DetailRelatedField"""
+
+    def __init__(self, view_name=None, view_name_pattern=None, **kwargs):
+        if view_name is None:
+            # set view name to prevent a DRF assertion that view_name is not None
+            # Anything that accesses self.view_name after __init__
+            # needs to have it set before being called. Unfortunately, a model instance
+            # is required to derive this value, so we can't make a view_name property.
+            if view_name_pattern:
+                view_name = _MatchingRegexViewName(view_name_pattern)
+            else:
+                log.warn(
+                    _(
+                        "Please provide either 'view_name' or 'view_name_pattern' for {} on {}."
+                    ).format(self.__class__.__name__, traceback.extract_stack()[-4][2])
+                )
+                view_name = _MatchingRegexViewName(r".*")
+        super().__init__(view_name, **kwargs)
+
+    def _view_name(self, obj):
+        # this is probably memoizeable based on the model class if we want to get cachey
+        try:
+            obj = obj.cast()
+        except AttributeError:
+            # The normal message that comes up here is unhelpful, so do like other DRF
+            # fails do and be a little more helpful in the exception message.
+            msg = (
+                'Expected a detail model instance, not {}. Do you need to add "many=True" to '
+                "this field definition in its serializer?"
+            ).format(type(obj))
+            raise ValueError(msg)
+        return get_view_name_for_model(obj, "detail")
+
+    def get_url(self, obj, view_name, request, *args, **kwargs):
+        # ignore the passed in view name and return the url to the cast unit, not the generic unit
+        view_name = self._view_name(obj)
+        return super().get_url(obj, view_name, request, *args, **kwargs)
+
+
+# Fields
+
+
+class IdentityField(
+    HrefFieldMixin,
+    serializers.HyperlinkedIdentityField,
+):
+    """IdentityField for use in the pulp_href field of non-Master/Detail Serializers.
+
+    When using this field on a serializer, it will serialize the related resource as a relative URL.
+    """
+
+
+class RelatedField(
+    HrefFieldMixin,
+    serializers.HyperlinkedRelatedField,
+):
+    """RelatedField when relating to non-Master/Detail models
+
+    When using this field on a serializer, it will serialize the related resource as a relative URL.
+    """
+
+
+class RelatedResourceField(RelatedField):
+    """RelatedResourceField when relating a Resource object models.
+
+    This field should be used to relate a list of non-homogeneous resources. e.g.:
+    CreatedResource and ExportedResource models that store relationships to arbitrary
+    resources.
+
+    Specific implementation requires the model to be defined in the Meta:.
+    """
+
+    def to_representation(self, data):
+        # If the content object was deleted
+        if data.content_object is None:
+            return None
+        try:
+            if not data.content_object.complete:
+                return None
+        except AttributeError:
+            pass
+
+        # query parameters can be ignored because we are looking just for 'pulp_href'; still,
+        # we need to use the request object due to contextual references required by some
+        # serializers
+        request = get_request_without_query_params(self.context)
+
+        viewset = get_viewset_for_model(data.content_object)
+        serializer = viewset.serializer_class(data.content_object, context={"request": request})
+        return serializer.data.get("pulp_href")
+
+
+class DetailIdentityField(_DetailFieldMixin, serializers.HyperlinkedIdentityField):
+    """IdentityField for use in the pulp_href field of Master/Detail Serializers
+
+    When using this field on a Serializer, it will automatically cast objects to their Detail type
+    base on the Serializer's Model before generating URLs for them.
+
+    Subclasses must indicate the Master model they represent by declaring a queryset
+    in their class body, usually <MasterModelImplementation>.objects.all().
+    """
+
+
+class DetailRelatedField(_DetailFieldMixin, serializers.HyperlinkedRelatedField):
+    """RelatedField for use when relating to Master/Detail models
+
+    When using this field on a Serializer, relate it to the Master model in a
+    Master/Detail relationship, and it will automatically cast objects to their Detail type
+    before generating URLs for them.
+
+    Subclasses must indicate the Master model they represent by declaring a queryset
+    in their class body, usually <MasterModelImplementation>.objects.all().
+    """
+
+    def get_object(self, *args, **kwargs):
+        # return the cast object, not the generic contentunit
+        return super().get_object(*args, **kwargs).cast()
+
+    def use_pk_only_optimization(self):
+        """
+        If the lookup field is `pk`, DRF substitutes a PKOnlyObject as an optimization. This
+        optimization breaks with Detail fields like this one which need access to their Meta
+        class to get the relevant `view_name`.
+        """
+        return False
+
+
+class NestedIdentityField(HrefFieldMixin, NestedHyperlinkedIdentityField):
+    """NestedIdentityField for use with nested resources.
+
+    When using this field in a serializer, it serializes the resource as a relative URL.
+    """
+
+
+class NestedRelatedField(
+    HrefFieldMixin,
+    NestedHyperlinkedRelatedField,
+):
+    """NestedRelatedField for use when relating to nested resources.
+
+    When using this field in a serializer, it serializes the related resource as a relative URL.
+    """
+
+
+# Serializer mixins
+
+
 def validate_unknown_fields(initial_data, defined_fields):
     """
     This will raise a `ValidationError` if a serializer is passed fields that are unknown.
@@ -49,6 +231,36 @@ def validate(self, data):
         return data
 
 
+class HiddenFieldsMixin(serializers.Serializer):
+    """
+    Adds a list field of hidden (write only) fields and whether their values are set
+    so clients can tell if they are overwriting an existing value.
+    For example this could be any sensitive information such as a password, name or token.
+    The list contains dictionaries with keys `name` and `is_set`.
+    """
+
+    hidden_fields = serializers.SerializerMethodField(
+        help_text=_("List of hidden (write only) fields")
+    )
+
+    def get_hidden_fields(
+        self, obj
+    ) -> List[TypedDict("hidden_fields", {"name": str, "is_set": bool})]:
+        hidden_fields = []
+
+        # returns false if field is "" or None
+        def _is_set(field_name):
+            field_value = getattr(obj, field_name)
+            return field_value != "" and field_value is not None
+
+        fields = self.get_fields()
+        for field_name in fields:
+            if fields[field_name].write_only:
+                hidden_fields.append({"name": field_name, "is_set": _is_set(field_name)})
+
+        return hidden_fields
+
+
 class GetOrCreateSerializerMixin:
     """A mixin that provides a get_or_create with validation in the serializer"""
 
@@ -71,6 +283,9 @@ def get_or_create(cls, natural_key, default_values=None):
         return result
 
 
+# Serializers
+
+
 class ModelSerializer(
     ValidateFieldsMixin, QueryFieldsMixin, serializers.HyperlinkedModelSerializer
 ):
@@ -214,180 +429,6 @@ def update(self, instance, validated_data):
         return instance
 
 
-class _MatchingRegexViewName(object):
-    """This is a helper class to help defining object matching rules for master-detail.
-
-    If you can be specific, please specify the `view_name`, but if you cannot, this allows
-    you to specify a regular expression like .e.g. `r"repositories(-.*/.*)?-detail"` to
-    identify whether the provided resources viewn name belongs to any repository type.
-    """
-
-    __slots__ = ("pattern",)
-
-    def __init__(self, pattern):
-        self.pattern = pattern
-
-    def __repr__(self):
-        return f'{self.__class__.__name__}(r"{self.pattern}")'
-
-    def __eq__(self, other):
-        return re.fullmatch(self.pattern, other) is not None
-
-
-class _DetailFieldMixin:
-    """Mixin class containing code common to DetailIdentityField and DetailRelatedField"""
-
-    def __init__(self, view_name=None, view_name_pattern=None, **kwargs):
-        if view_name is None:
-            # set view name to prevent a DRF assertion that view_name is not None
-            # Anything that accesses self.view_name after __init__
-            # needs to have it set before being called. Unfortunately, a model instance
-            # is required to derive this value, so we can't make a view_name property.
-            if view_name_pattern:
-                view_name = _MatchingRegexViewName(view_name_pattern)
-            else:
-                log.warn(
-                    _(
-                        "Please provide either 'view_name' or 'view_name_pattern' for {} on {}."
-                    ).format(self.__class__.__name__, traceback.extract_stack()[-4][2])
-                )
-                view_name = _MatchingRegexViewName(r".*")
-        super().__init__(view_name, **kwargs)
-
-    def _view_name(self, obj):
-        # this is probably memoizeable based on the model class if we want to get cachey
-        try:
-            obj = obj.cast()
-        except AttributeError:
-            # The normal message that comes up here is unhelpful, so do like other DRF
-            # fails do and be a little more helpful in the exception message.
-            msg = (
-                'Expected a detail model instance, not {}. Do you need to add "many=True" to '
-                "this field definition in its serializer?"
-            ).format(type(obj))
-            raise ValueError(msg)
-        return get_view_name_for_model(obj, "detail")
-
-    def get_url(self, obj, view_name, request, *args, **kwargs):
-        # ignore the passed in view name and return the url to the cast unit, not the generic unit
-        request = None
-        view_name = self._view_name(obj)
-        return super().get_url(obj, view_name, request, *args, **kwargs)
-
-
-class IdentityField(serializers.HyperlinkedIdentityField):
-    """IdentityField for use in the pulp_href field of non-Master/Detail Serializers.
-
-    The get_url method is overriden so relative URLs are returned.
-    """
-
-    def get_url(self, obj, view_name, request, *args, **kwargs):
-        # ignore the passed in view name and return the url to the cast unit, not the generic unit
-        request = None
-        return super().get_url(obj, view_name, request, *args, **kwargs)
-
-
-class RelatedField(serializers.HyperlinkedRelatedField):
-    """RelatedField when relating to non-Master/Detail models
-
-    When using this field on a serializer, it will serialize the related resource as a relative URL.
-    """
-
-    def get_url(self, obj, view_name, request, *args, **kwargs):
-        # ignore the passed in view name and return the url to the cast unit, not the generic unit
-        request = None
-        return super().get_url(obj, view_name, request, *args, **kwargs)
-
-
-class RelatedResourceField(RelatedField):
-    """RelatedResourceField when relating a Resource object models.
-
-    This field should be used to relate a list of non-homogeneous resources. e.g.:
-    CreatedResource and ExportedResource models that store relationships to arbitrary
-    resources.
-
-    Specific implementation requires the model to be defined in the Meta:.
-    """
-
-    def to_representation(self, data):
-        # If the content object was deleted
-        if data.content_object is None:
-            return None
-        try:
-            if not data.content_object.complete:
-                return None
-        except AttributeError:
-            pass
-
-        # query parameters can be ignored because we are looking just for 'pulp_href'; still,
-        # we need to use the request object due to contextual references required by some
-        # serializers
-        request = get_request_without_query_params(self.context)
-
-        viewset = get_viewset_for_model(data.content_object)
-        serializer = viewset.serializer_class(data.content_object, context={"request": request})
-        return serializer.data.get("pulp_href")
-
-
-class DetailIdentityField(_DetailFieldMixin, serializers.HyperlinkedIdentityField):
-    """IdentityField for use in the pulp_href field of Master/Detail Serializers
-
-    When using this field on a Serializer, it will automatically cast objects to their Detail type
-    base on the Serializer's Model before generating URLs for them.
-
-    Subclasses must indicate the Master model they represent by declaring a queryset
-    in their class body, usually <MasterModelImplementation>.objects.all().
-    """
-
-
-class DetailRelatedField(_DetailFieldMixin, serializers.HyperlinkedRelatedField):
-    """RelatedField for use when relating to Master/Detail models
-
-    When using this field on a Serializer, relate it to the Master model in a
-    Master/Detail relationship, and it will automatically cast objects to their Detail type
-    before generating URLs for them.
-
-    Subclasses must indicate the Master model they represent by declaring a queryset
-    in their class body, usually <MasterModelImplementation>.objects.all().
-    """
-
-    def get_object(self, *args, **kwargs):
-        # return the cast object, not the generic contentunit
-        return super().get_object(*args, **kwargs).cast()
-
-    def use_pk_only_optimization(self):
-        """
-        If the lookup field is `pk`, DRF substitutes a PKOnlyObject as an optimization. This
-        optimization breaks with Detail fields like this one which need access to their Meta
-        class to get the relevant `view_name`.
-        """
-        return False
-
-
-class NestedIdentityField(NestedHyperlinkedIdentityField):
-    """NestedIdentityField for use with nested resources.
-
-    When using this field in a serializer, it serializes the resource as a relative URL.
-    """
-
-    def get_url(self, obj, view_name, request, *args, **kwargs):
-        # ignore the passed in view name and return the url to the cast unit, not the generic unit
-        request = None
-        return super().get_url(obj, view_name, request, *args, **kwargs)
-
-
-class NestedRelatedField(NestedHyperlinkedRelatedField):
-    """NestedRelatedField for use when relating to nested resources.
-
-    When using this field in a serializer, it serializes the related resource as a relative URL.
-    """
-
-    def get_url(self, obj, view_name, request, *args, **kwargs):
-        # ignore the passed in view name and return the url to the cast unit, not the generic unit
-        request = None
-        return super().get_url(obj, view_name, request, *args, **kwargs)
-
-
 class AsyncOperationResponseSerializer(serializers.Serializer):
     """
     Serializer for asynchronous operations.
@@ -414,33 +455,3 @@ class TaskGroupOperationResponseSerializer(serializers.Serializer):
         view_name="task-groups-detail",
         allow_null=False,
     )
-
-
-class HiddenFieldsMixin(serializers.Serializer):
-    """
-    Adds a list field of hidden (write only) fields and whether their values are set
-    so clients can tell if they are overwriting an existing value.
-    For example this could be any sensitive information such as a password, name or token.
-    The list contains dictionaries with keys `name` and `is_set`.
-    """
-
-    hidden_fields = serializers.SerializerMethodField(
-        help_text=_("List of hidden (write only) fields")
-    )
-
-    def get_hidden_fields(
-        self, obj
-    ) -> List[TypedDict("hidden_fields", {"name": str, "is_set": bool})]:
-        hidden_fields = []
-
-        # returns false if field is "" or None
-        def _is_set(field_name):
-            field_value = getattr(obj, field_name)
-            return field_value != "" and field_value is not None
-
-        fields = self.get_fields()
-        for field_name in fields:
-            if fields[field_name].write_only:
-                hidden_fields.append({"name": field_name, "is_set": _is_set(field_name)})
-
-        return hidden_fields


Test patch: