1"""
2Reverse relation descriptors for explicit reverse relation declarations.
3
4This module contains descriptors for the reverse side of ForeignKeyField and
5ManyToManyField relations, allowing explicit declaration of reverse accessors
6without relying on automatic related_name generation.
7"""
8
9from __future__ import annotations
10
11from typing import TYPE_CHECKING, Any, Generic, TypeVar
12
13if TYPE_CHECKING:
14 from plain.postgres import Model
15 from plain.postgres.fields.related_managers import BaseRelatedManager
16 from plain.postgres.query import QuerySet
17
18T = TypeVar("T", bound="Model")
19# Default to QuerySet[Any] so users can omit the second type parameter
20QS = TypeVar("QS", bound="QuerySet[Any]", default="QuerySet[Any]")
21
22
23class BaseReverseDescriptor(Generic[T, QS]):
24 """
25 Base class for reverse relation descriptors.
26
27 Provides common functionality for ReverseForeignKey and ReverseManyToMany
28 descriptors, including field resolution, validation, and the descriptor protocol.
29 """
30
31 def __init__(self, to: str | type[T], field: str):
32 self.to = to
33 self.field_name = field
34 self.name: str | None = None
35 self.model: type[Model] | None = None
36 self._resolved_model: type[T] | None = None
37 self._resolved_field: Any = None
38
39 def contribute_to_class(self, cls: type[Model], name: str) -> None:
40 """
41 Register this reverse relation with the model class.
42
43 Called by the model metaclass when the model is created.
44 """
45 self.name = name
46 self.model = cls
47
48 # Set the descriptor on the class
49 setattr(cls, name, self)
50
51 # Register this as a related object for prefetch support
52 # We'll do this lazily when the target model is resolved
53 from plain.postgres.fields.related import lazy_related_operation
54
55 def resolve_related_field(
56 parent_model: type[Model], related_model: type[T]
57 ) -> None:
58 """Resolve the target model and field, then register."""
59 self._resolved_model = related_model
60 try:
61 self._resolved_field = related_model._model_meta.get_field(
62 self.field_name
63 )
64 except Exception as e:
65 raise ValueError(
66 f"Field '{self.field_name}' not found on model "
67 f"'{related_model.__name__}' for {self._get_descriptor_type()} '{self.name}' "
68 f"on '{cls.__name__}'. Error: {e}"
69 )
70
71 # Validate that the field is the correct type
72 self._validate_field_type(related_model)
73
74 # Use lazy operation to handle circular dependencies
75 lazy_related_operation(resolve_related_field, cls, self.to)
76
77 def __get__(
78 self, instance: Model | None, owner: type[Model]
79 ) -> BaseReverseDescriptor[T, QS] | BaseRelatedManager[T, QS]:
80 """
81 Get the related manager when accessed on an instance.
82
83 When accessed on the class, returns the descriptor.
84 When accessed on an instance, returns a manager.
85 """
86 if instance is None:
87 return self
88
89 # Ensure the related model and field are resolved
90 if self._resolved_field is None or self.model is None:
91 model_name = self.model.__name__ if self.model else "Unknown"
92 raise ValueError(
93 f"{self._get_descriptor_type()} '{self.name}' on '{model_name}' "
94 f"has not been resolved yet. The target model may not be registered."
95 )
96
97 # _resolved_model is set alongside _resolved_field in resolve_related_field
98 assert self._resolved_model is not None, "Model should be resolved with field"
99
100 # Return a manager bound to this instance
101 return self._create_manager(instance)
102
103 def __set__(self, instance: Model, value: Any) -> None:
104 """Prevent direct assignment to reverse relations."""
105 raise TypeError(
106 f"Direct assignment to the reverse side of a {self._get_field_type()} "
107 f"('{self.name}') is prohibited. Use {self.name}.set() instead."
108 )
109
110 def _get_descriptor_type(self) -> str:
111 """Return the name of this descriptor type for error messages."""
112 raise NotImplementedError("Subclasses must implement _get_descriptor_type()")
113
114 def _get_field_type(self) -> str:
115 """Return the name of the forward field type for error messages."""
116 raise NotImplementedError("Subclasses must implement _get_field_type()")
117
118 def _validate_field_type(self, related_model: type[Model]) -> None:
119 """Validate that the resolved field is the correct type."""
120 raise NotImplementedError("Subclasses must implement _validate_field_type()")
121
122 def _create_manager(self, instance: Model) -> Any:
123 """Create and return the appropriate manager for this instance."""
124 raise NotImplementedError("Subclasses must implement _create_manager()")
125
126
127class ReverseForeignKey(BaseReverseDescriptor[T, QS]):
128 """
129 Descriptor for the reverse side of a ForeignKeyField relation.
130
131 Provides access to the related instances on the "one" side of a one-to-many
132 relationship.
133
134 Example:
135 class Parent(Model):
136 # Basic usage (uses default QuerySet[Child])
137 children: ReverseForeignKey[Child, QuerySet[Child]] = ReverseForeignKey(to="Child", field="parent")
138
139 # With custom QuerySet
140 children: ReverseForeignKey[Child, ChildQuerySet] = ReverseForeignKey(to="Child", field="parent")
141
142 class Child(Model):
143 parent: Parent = ForeignKeyField(Parent, on_delete=models.CASCADE)
144
145 Args:
146 to: The related model (string name or model class)
147 field: The field name on the related model that points back to this model
148 """
149
150 def _get_descriptor_type(self) -> str:
151 return "ReverseForeignKey"
152
153 def _get_field_type(self) -> str:
154 return "ForeignKey"
155
156 def _validate_field_type(self, related_model: type[Model]) -> None:
157 """Validate that the field is a ForeignKey."""
158 from plain.postgres.fields.related import ForeignKeyField
159
160 if not isinstance(self._resolved_field, ForeignKeyField):
161 raise ValueError(
162 f"Field '{self.field_name}' on '{related_model.__name__}' is not a "
163 f"ForeignKey. ReverseForeignKey requires a ForeignKeyField field."
164 )
165
166 def _create_manager(self, instance: Model) -> Any:
167 """Create a ReverseForeignKeyManager for this instance."""
168 from plain.postgres.fields.related_managers import ReverseForeignKeyManager
169
170 assert self._resolved_model is not None
171 return ReverseForeignKeyManager(
172 instance=instance,
173 field=self._resolved_field,
174 related_model=self._resolved_model,
175 )
176
177
178class ReverseManyToMany(BaseReverseDescriptor[T, QS]):
179 """
180 Descriptor for the reverse side of a ManyToManyField relation.
181
182 Provides access to the related instances on the reverse side of a many-to-many
183 relationship.
184
185 Example:
186 class Feature(Model):
187 # Basic usage (uses default QuerySet[Car])
188 cars: ReverseManyToMany[Car, QuerySet[Car]] = ReverseManyToMany(to="Car", field="features")
189
190 # With custom QuerySet
191 cars: ReverseManyToMany[Car, CarQuerySet] = ReverseManyToMany(to="Car", field="features")
192
193 class Car(Model):
194 features: ManyToManyField[Feature] = ManyToManyField(Feature, through=CarFeature)
195
196 Args:
197 to: The related model (string name or model class)
198 field: The field name on the related model that points to this model
199 """
200
201 def _get_descriptor_type(self) -> str:
202 return "ReverseManyToMany"
203
204 def _get_field_type(self) -> str:
205 return "ManyToManyField"
206
207 def _validate_field_type(self, related_model: type[Model]) -> None:
208 """Validate that the field is a ManyToManyField."""
209 from plain.postgres.fields.related import ManyToManyField
210
211 if not isinstance(self._resolved_field, ManyToManyField):
212 raise ValueError(
213 f"Field '{self.field_name}' on '{related_model.__name__}' is not a "
214 f"ManyToManyField. ReverseManyToMany requires a ManyToManyField."
215 )
216
217 def _create_manager(self, instance: Model) -> Any:
218 """Create a ManyToManyManager for this instance."""
219 from plain.postgres.fields.related_managers import ManyToManyManager
220
221 assert self._resolved_model is not None
222 return ManyToManyManager(
223 instance=instance,
224 field=self._resolved_field,
225 through=self._resolved_field.remote_field.through,
226 related_model=self._resolved_model,
227 is_reverse=True,
228 symmetrical=False,
229 )