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