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