Plain is headed towards 1.0! Subscribe for development updates →

  1"""
  2Accessors for related objects.
  3
  4When a field defines a relation between two models, each model class provides
  5an attribute to access related instances of the other model class (unless the
  6reverse accessor has been disabled with related_name='+').
  7
  8Accessors are implemented as descriptors in order to customize access and
  9assignment. This module defines the descriptor classes.
 10
 11Forward accessors follow foreign keys. Reverse accessors trace them back. For
 12example, with the following models::
 13
 14    class Parent(Model):
 15        pass
 16
 17    class Child(Model):
 18        parent = ForeignKey(Parent, related_name='children')
 19
 20 ``child.parent`` is a forward many-to-one relation. ``parent.children`` is a
 21reverse many-to-one relation.
 22
 231. Related instance on the forward side of a many-to-one relation:
 24   ``ForwardManyToOneDescriptor``.
 25
 26   Uniqueness of foreign key values is irrelevant to accessing the related
 27   instance, making the many-to-one and one-to-one cases identical as far as
 28   the descriptor is concerned. The constraint is checked upstream (unicity
 29   validation in forms) or downstream (unique indexes in the database).
 30
 312. Related objects manager for related instances on the reverse side of a
 32   many-to-one relation: ``ReverseManyToOneDescriptor``.
 33
 34   Unlike the previous two classes, this one provides access to a collection
 35   of objects. It returns a manager rather than an instance.
 36
 373. Related objects manager for related instances on the forward or reverse
 38   sides of a many-to-many relation: ``ManyToManyDescriptor``.
 39
 40   Many-to-many relations are symmetrical. The syntax of Plain models
 41   requires declaring them on one side but that's an implementation detail.
 42   They could be declared on the other side without any change in behavior.
 43   Therefore the forward and reverse descriptors can be the same.
 44
 45   If you're looking for ``ForwardManyToManyDescriptor`` or
 46   ``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
 47"""
 48
 49from __future__ import annotations
 50
 51from functools import cached_property
 52from typing import Any
 53
 54from plain.models.query import QuerySet
 55from plain.models.query_utils import DeferredAttribute
 56from plain.utils.functional import LazyObject
 57
 58from .related_managers import (
 59    ForwardManyToManyManager,
 60    ReverseManyToManyManager,
 61    ReverseManyToOneManager,
 62)
 63
 64
 65class ForeignKeyDeferredAttribute(DeferredAttribute):
 66    def __set__(self, instance: Any, value: Any) -> None:
 67        if instance.__dict__.get(self.field.attname) != value and self.field.is_cached(
 68            instance
 69        ):
 70            self.field.delete_cached_value(instance)
 71        instance.__dict__[self.field.attname] = value
 72
 73
 74class ForwardManyToOneDescriptor:
 75    """
 76    Accessor to the related object on the forward side of a many-to-one relation.
 77
 78    In the example::
 79
 80        class Child(Model):
 81            parent = ForeignKey(Parent, related_name='children')
 82
 83    ``Child.parent`` is a ``ForwardManyToOneDescriptor`` instance.
 84    """
 85
 86    def __init__(self, field_with_rel: Any) -> None:
 87        self.field = field_with_rel
 88
 89    @cached_property
 90    def RelatedObjectDoesNotExist(self) -> type:
 91        # The exception can't be created at initialization time since the
 92        # related model might not be resolved yet; `self.field.model` might
 93        # still be a string model reference.
 94        return type(
 95            "RelatedObjectDoesNotExist",
 96            (self.field.remote_field.model.DoesNotExist, AttributeError),
 97            {
 98                "__module__": self.field.model.__module__,
 99                "__qualname__": f"{self.field.model.__qualname__}.{self.field.name}.RelatedObjectDoesNotExist",
100            },
101        )
102
103    def is_cached(self, instance: Any) -> bool:
104        return self.field.is_cached(instance)
105
106    def get_queryset(self) -> QuerySet:
107        qs = self.field.remote_field.model._model_meta.base_queryset
108        return qs.all()
109
110    def get_prefetch_queryset(
111        self, instances: list[Any], queryset: QuerySet | None = None
112    ) -> tuple[QuerySet, Any, Any, bool, str, bool]:
113        if queryset is None:
114            queryset = self.get_queryset()
115
116        rel_obj_attr = self.field.get_foreign_related_value
117        instance_attr = self.field.get_local_related_value
118        instances_dict = {instance_attr(inst): inst for inst in instances}
119        related_field = self.field.foreign_related_fields[0]
120        remote_field = self.field.remote_field
121
122        # FIXME: This will need to be revisited when we introduce support for
123        # composite fields. In the meantime we take this practical approach to
124        # solve a regression on 1.6 when the reverse manager in hidden
125        # (related_name ends with a '+'). Refs #21410.
126        # The check for len(...) == 1 is a special case that allows the query
127        # to be join-less and smaller. Refs #21760.
128        if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
129            query = {
130                f"{related_field.name}__in": {
131                    instance_attr(inst)[0] for inst in instances
132                }
133            }
134        else:
135            query = {f"{self.field.related_query_name()}__in": instances}
136        queryset = queryset.filter(**query)
137
138        # Since we're going to assign directly in the cache,
139        # we must manage the reverse relation cache manually.
140        if not remote_field.multiple:
141            for rel_obj in queryset:
142                instance = instances_dict[rel_obj_attr(rel_obj)]
143                remote_field.set_cached_value(rel_obj, instance)
144        return (
145            queryset,
146            rel_obj_attr,
147            instance_attr,
148            True,
149            self.field.get_cache_name(),
150            False,
151        )
152
153    def get_object(self, instance: Any) -> Any:
154        qs = self.get_queryset()
155        # Assuming the database enforces foreign keys, this won't fail.
156        return qs.get(self.field.get_reverse_related_filter(instance))
157
158    def __get__(
159        self, instance: Any | None, cls: type | None = None
160    ) -> ForwardManyToOneDescriptor | Any | None:
161        """
162        Get the related instance through the forward relation.
163
164        With the example above, when getting ``child.parent``:
165
166        - ``self`` is the descriptor managing the ``parent`` attribute
167        - ``instance`` is the ``child`` instance
168        - ``cls`` is the ``Child`` class (we don't need it)
169        """
170        if instance is None:
171            return self
172
173        # The related instance is loaded from the database and then cached
174        # by the field on the model instance state. It can also be pre-cached
175        # by the reverse accessor.
176        try:
177            rel_obj = self.field.get_cached_value(instance)
178        except KeyError:
179            has_value = None not in self.field.get_local_related_value(instance)
180            rel_obj = None
181
182            if rel_obj is None and has_value:
183                rel_obj = self.get_object(instance)
184                remote_field = self.field.remote_field
185                # If this is a one-to-one relation, set the reverse accessor
186                # cache on the related object to the current instance to avoid
187                # an extra SQL query if it's accessed later on.
188                if not remote_field.multiple:
189                    remote_field.set_cached_value(rel_obj, instance)
190            self.field.set_cached_value(instance, rel_obj)
191
192        if rel_obj is None and not self.field.allow_null:
193            raise self.RelatedObjectDoesNotExist(
194                f"{self.field.model.__name__} has no {self.field.name}."
195            )
196        else:
197            return rel_obj
198
199    def __set__(self, instance: Any, value: Any) -> None:
200        """
201        Set the related instance through the forward relation.
202
203        With the example above, when setting ``child.parent = parent``:
204
205        - ``self`` is the descriptor managing the ``parent`` attribute
206        - ``instance`` is the ``child`` instance
207        - ``value`` is the ``parent`` instance on the right of the equal sign
208        """
209        # If value is a LazyObject, force its evaluation. For ForeignKey fields,
210        # the value should only be None or a model instance, never a boolean or
211        # other type.
212        if isinstance(value, LazyObject):
213            # This forces evaluation: if it's None, value becomes None;
214            # if it's a User instance, value becomes that instance.
215            value = value if value else None
216
217        # An object must be an instance of the related class.
218        if value is not None and not isinstance(value, self.field.remote_field.model):
219            raise ValueError(
220                f'Cannot assign "{value!r}": "{instance.model_options.object_name}.{self.field.name}" must be a "{self.field.remote_field.model.model_options.object_name}" instance.'
221            )
222        remote_field = self.field.remote_field
223        # If we're setting the value of a OneToOneField to None, we need to clear
224        # out the cache on any old related object. Otherwise, deleting the
225        # previously-related object will also cause this object to be deleted,
226        # which is wrong.
227        if value is None:
228            # Look up the previously-related object, which may still be available
229            # since we've not yet cleared out the related field.
230            # Use the cache directly, instead of the accessor; if we haven't
231            # populated the cache, then we don't care - we're only accessing
232            # the object to invalidate the accessor cache, so there's no
233            # need to populate the cache just to expire it again.
234            related = self.field.get_cached_value(instance, default=None)
235
236            # If we've got an old related object, we need to clear out its
237            # cache. This cache also might not exist if the related object
238            # hasn't been accessed yet.
239            if related is not None:
240                remote_field.set_cached_value(related, None)
241
242            for lh_field, rh_field in self.field.related_fields:
243                setattr(instance, lh_field.attname, None)
244
245        # Set the values of the related field.
246        else:
247            for lh_field, rh_field in self.field.related_fields:
248                setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
249
250        # Set the related instance cache used by __get__ to avoid an SQL query
251        # when accessing the attribute we just set.
252        self.field.set_cached_value(instance, value)
253
254        # If this is a one-to-one relation, set the reverse accessor cache on
255        # the related object to the current instance to avoid an extra SQL
256        # query if it's accessed later on.
257        if value is not None and not remote_field.multiple:
258            remote_field.set_cached_value(value, instance)
259
260    def __reduce__(self) -> tuple[Any, tuple[Any, str]]:
261        """
262        Pickling should return the instance attached by self.field on the
263        model, not a new copy of that descriptor. Use getattr() to retrieve
264        the instance directly from the model.
265        """
266        return getattr, (self.field.model, self.field.name)
267
268
269class RelationDescriptorBase:
270    """
271    Base class for relation descriptors that don't allow direct assignment.
272
273    This is used for descriptors that manage collections of related objects
274    (reverse FK and M2M relations). Forward FK relations don't inherit from
275    this because they allow direct assignment.
276    """
277
278    def __init__(self, rel: Any) -> None:
279        self.rel = rel
280        self.field = rel.field
281
282    def __get__(
283        self, instance: Any | None, cls: type | None = None
284    ) -> RelationDescriptorBase | Any:
285        """
286        Get the related manager when the descriptor is accessed.
287
288        Subclasses must implement get_related_manager().
289        """
290        if instance is None:
291            return self
292        return self.get_related_manager(instance)
293
294    def get_related_manager(self, instance: Any) -> Any:
295        """Return the appropriate manager for this relation."""
296        raise NotImplementedError(
297            f"{self.__class__.__name__} must implement get_related_manager()"
298        )
299
300    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
301        """Return parameters for the error message when direct assignment is attempted."""
302        raise NotImplementedError(
303            f"{self.__class__.__name__} must implement _get_set_deprecation_msg_params()"
304        )
305
306    def __set__(self, instance: Any, value: Any) -> None:
307        """Prevent direct assignment to the relation."""
308        raise TypeError(
309            "Direct assignment to the {} is prohibited. Use {}.set() instead.".format(
310                *self._get_set_deprecation_msg_params()
311            ),
312        )
313
314
315class ReverseManyToOneDescriptor(RelationDescriptorBase):
316    """
317    Accessor to the related objects manager on the reverse side of a
318    many-to-one relation.
319
320    In the example::
321
322        class Child(Model):
323            parent = ForeignKey(Parent, related_name='children')
324
325    ``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
326
327    Most of the implementation is delegated to the ReverseManyToOneManager class.
328    """
329
330    def get_related_manager(self, instance: Any) -> ReverseManyToOneManager:
331        """Return the ReverseManyToOneManager for this relation."""
332        return ReverseManyToOneManager(instance, self.rel)
333
334    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
335        return (
336            "reverse side of a related set",
337            self.rel.get_accessor_name(),
338        )
339
340
341class ForwardManyToManyDescriptor(RelationDescriptorBase):
342    """
343    Accessor to the related objects manager on the forward side of a
344    many-to-many relation.
345
346    In the example::
347
348        class Pizza(Model):
349            toppings = ManyToManyField(Topping, related_name='pizzas')
350
351    ``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
352    """
353
354    @property
355    def through(self) -> Any:
356        # through is provided so that you have easy access to the through
357        # model (Book.authors.through) for inlines, etc. This is done as
358        # a property to ensure that the fully resolved value is returned.
359        return self.rel.through
360
361    def get_related_manager(self, instance: Any) -> ForwardManyToManyManager:
362        """Return the ForwardManyToManyManager for this relation."""
363        return ForwardManyToManyManager(instance, self.rel)
364
365    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
366        return (
367            "forward side of a many-to-many set",
368            self.field.name,
369        )
370
371
372class ReverseManyToManyDescriptor(RelationDescriptorBase):
373    """
374    Accessor to the related objects manager on the reverse side of a
375    many-to-many relation.
376
377    In the example::
378
379        class Pizza(Model):
380            toppings = ManyToManyField(Topping, related_name='pizzas')
381
382    ``Topping.pizzas`` is a ``ReverseManyToManyDescriptor`` instance.
383    """
384
385    @property
386    def through(self) -> Any:
387        # through is provided so that you have easy access to the through
388        # model (Book.authors.through) for inlines, etc. This is done as
389        # a property to ensure that the fully resolved value is returned.
390        return self.rel.through
391
392    def get_related_manager(self, instance: Any) -> ReverseManyToManyManager:
393        """Return the ReverseManyToManyManager for this relation."""
394        return ReverseManyToManyManager(instance, self.rel)
395
396    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
397        return (
398            "reverse side of a many-to-many set",
399            self.rel.get_accessor_name(),
400        )