Plain is headed towards 1.0! Subscribe for development updates →

  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        )