v0.156.1
  1from __future__ import annotations
  2
  3import copy
  4import inspect
  5from collections import defaultdict
  6from collections.abc import Iterable
  7from functools import cached_property
  8from typing import TYPE_CHECKING, Any, Literal, overload
  9
 10from plain.postgres.exceptions import FieldDoesNotExist
 11from plain.postgres.query import QuerySet
 12from plain.postgres.registry import models_registry as default_models_registry
 13from plain.utils.datastructures import ImmutableList
 14
 15if TYPE_CHECKING:
 16    from plain.postgres.base import Model
 17    from plain.postgres.constraints import BaseConstraint
 18    from plain.postgres.fields import Field
 19    from plain.postgres.fields.related import ManyToManyField, RelatedField
 20    from plain.postgres.fields.reverse_related import ForeignObjectRel
 21
 22EMPTY_RELATION_TREE = ()
 23
 24IMMUTABLE_WARNING = (
 25    "The return type of '%s' should never be mutated. If you want to manipulate this "
 26    "list for your own use, make a copy first."
 27)
 28
 29
 30def make_immutable_fields_list[T](name: str, data: Iterable[T]) -> ImmutableList[T]:
 31    return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
 32
 33
 34class Meta:
 35    """
 36    Model metadata descriptor and container.
 37
 38    Acts as both a descriptor (for lazy initialization and access control)
 39    and the actual metadata instance (cached per model class).
 40    """
 41
 42    FORWARD_PROPERTIES = {
 43        "fields",
 44        "many_to_many",
 45        "concrete_fields",
 46        "local_concrete_fields",
 47        "_non_pk_concrete_field_names",
 48        "_forward_fields_map",
 49        "base_queryset",
 50    }
 51    REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
 52
 53    # Type annotations for attributes set in _create_and_cache
 54    # These exist on cached instances, not on the descriptor itself
 55    model: type[Model]
 56    models_registry: Any
 57    _get_fields_cache: dict[Any, Any]
 58    local_fields: list[Field]
 59    local_many_to_many: list[ManyToManyField]
 60
 61    def __init__(self, models_registry: Any | None = None):
 62        """
 63        Initialize the descriptor with optional configuration.
 64
 65        This is called ONCE when defining the base Model class.
 66        The descriptor then creates cached instances per model subclass.
 67        """
 68        self._models_registry = models_registry
 69        self._cache: dict[type[Model], Meta] = {}
 70
 71    def __get__(self, instance: Any, owner: type[Model]) -> Meta:
 72        """
 73        Descriptor protocol - returns cached Meta instance for the model class.
 74
 75        This is called when accessing Model._model_meta and returns a per-class
 76        cached instance created by _create_and_cache().
 77
 78        Can be accessed from both class and instances:
 79        - MyModel._model_meta (class access)
 80        - my_instance._model_meta (instance access - returns class's metadata)
 81        """
 82        # Allow instance access - just return the class's metadata
 83        if instance is not None:
 84            owner = instance.__class__
 85
 86        # Skip for the base Model class - return descriptor
 87        if owner.__name__ == "Model" and owner.__module__ == "plain.postgres.base":
 88            return self
 89
 90        # Return cached instance or create new one
 91        if owner not in self._cache:
 92            # Create the instance and cache it BEFORE field contribution
 93            # to avoid infinite recursion when fields access cls._model_meta
 94            return self._create_and_cache(owner)
 95
 96        return self._cache[owner]
 97
 98    def _create_and_cache(self, model: type[Model]) -> Meta:
 99        """Create Meta instance and cache it before field contribution."""
100        # Create instance without calling __init__
101        instance = Meta.__new__(Meta)
102
103        # Initialize basic model-specific state
104        instance.model = model
105        instance.models_registry = self._models_registry or default_models_registry
106        instance._get_fields_cache = {}
107        instance.local_fields = []
108        instance.local_many_to_many = []
109
110        # Cache the instance BEFORE processing fields to prevent recursion
111        self._cache[model] = instance
112
113        # Now process fields - they can safely access cls._model_meta
114        seen_attrs = set()
115        for klass in model.__mro__:
116            for attr_name in list(klass.__dict__.keys()):
117                if attr_name.startswith("_") or attr_name in seen_attrs:
118                    continue
119                seen_attrs.add(attr_name)
120
121                attr_value = klass.__dict__[attr_name]
122
123                if not inspect.isclass(attr_value) and hasattr(
124                    attr_value, "contribute_to_class"
125                ):
126                    if attr_name not in model.__dict__:
127                        field = copy.deepcopy(attr_value)
128                    else:
129                        field = attr_value
130                    field.contribute_to_class(model, attr_name)
131
132        # Sort fields: primary key first, then alphabetically by name
133        instance.local_fields.sort(key=lambda f: (not f.primary_key, f.name or ""))
134        instance.local_many_to_many.sort(key=lambda f: f.name or "")
135
136        return instance
137
138    @property
139    def base_queryset(self) -> QuerySet:
140        """
141        The base queryset is used by Plain's internal operations like cascading
142        deletes, migrations, and related object lookups. It provides access to
143        all objects in the database without any filtering, ensuring Plain can
144        always see the complete dataset when performing framework operations.
145
146        Unlike user-defined querysets which may filter results (e.g. only active
147        objects), the base queryset must never filter out rows to prevent
148        incomplete results in related queries.
149        """
150        return QuerySet.from_model(self.model)
151
152    def add_field(self, field: Field) -> None:
153        from plain.postgres.fields.related import ManyToManyField, RelatedField
154
155        if isinstance(field, ManyToManyField):
156            self.local_many_to_many.append(field)
157        else:
158            self.local_fields.append(field)
159
160        # If the field being added is a relation to another known field,
161        # expire the cache on this field and the forward cache on the field
162        # being referenced, because there will be new relationships in the
163        # cache. Otherwise, expire the cache of references *to* this field.
164        # The mechanism for getting at the related model is slightly odd -
165        # ideally, we'd just ask for field.related_model. However, related_model
166        # is a cached property, and all the models haven't been loaded yet, so
167        # we need to make sure we don't cache a string reference.
168        if isinstance(field, RelatedField) and field.remote_field.model:
169            try:
170                field.remote_field.model._model_meta._expire_cache(forward=False)
171            except AttributeError:
172                pass
173            self._expire_cache()
174        else:
175            self._expire_cache(reverse=False)
176
177    @cached_property
178    def fields(self) -> ImmutableList[Field]:
179        from plain.postgres.fields.related import RelatedField
180
181        """
182        Return a list of all forward fields on the model and its parents,
183        excluding ManyToManyFields.
184
185        Private API intended only to be used by Plain itself; get_fields()
186        combined with filtering of field properties is the public API for
187        obtaining this field list.
188        """
189
190        # For legacy reasons, the fields property should only contain forward
191        # fields that are not private or with a m2m cardinality.
192        def is_not_an_m2m_field(f: Any) -> bool:
193            from plain.postgres.fields.related import ManyToManyField
194
195            return not isinstance(f, ManyToManyField)
196
197        def is_not_a_generic_relation(f: Any) -> bool:
198            from plain.postgres.fields.related import ForeignKeyField, ManyToManyField
199
200            # Only ForeignKeyField and ManyToManyField are valid RelatedFields
201            # Anything else is a generic relation
202            if not isinstance(f, RelatedField):
203                return True
204            return isinstance(f, ForeignKeyField | ManyToManyField)
205
206        return make_immutable_fields_list(
207            "fields",
208            (
209                f
210                for f in self._get_fields(reverse=False)
211                if is_not_an_m2m_field(f) and is_not_a_generic_relation(f)
212            ),
213        )
214
215    @cached_property
216    def concrete_fields(self) -> ImmutableList[Field]:
217        """
218        Return a list of all concrete fields on the model and its parents.
219
220        Private API intended only to be used by Plain itself; get_fields()
221        combined with filtering of field properties is the public API for
222        obtaining this field list.
223        """
224        return make_immutable_fields_list(
225            "concrete_fields", (f for f in self.fields if f.concrete)
226        )
227
228    @cached_property
229    def local_concrete_fields(self) -> ImmutableList[Field]:
230        """
231        Return a list of all concrete fields on the model.
232
233        Private API intended only to be used by Plain itself; get_fields()
234        combined with filtering of field properties is the public API for
235        obtaining this field list.
236        """
237        return make_immutable_fields_list(
238            "local_concrete_fields", (f for f in self.local_fields if f.concrete)
239        )
240
241    @cached_property
242    def many_to_many(self) -> ImmutableList[Field]:
243        """
244        Return a list of all many to many fields on the model and its parents.
245
246        Private API intended only to be used by Plain itself; get_fields()
247        combined with filtering of field properties is the public API for
248        obtaining this list.
249        """
250        from plain.postgres.fields.related import ManyToManyField
251
252        return make_immutable_fields_list(
253            "many_to_many",
254            (
255                f
256                for f in self._get_fields(reverse=False)
257                if isinstance(f, ManyToManyField)
258            ),
259        )
260
261    @cached_property
262    def related_objects(self) -> ImmutableList[ForeignObjectRel]:
263        """
264        Return all related objects pointing to the current model. The related
265        objects can come from a one-to-many or many-to-many field relation type.
266
267        Private API intended only to be used by Plain itself; get_fields()
268        combined with filtering of field properties is the public API for
269        obtaining this field list.
270        """
271        from plain.postgres.fields.reverse_related import ForeignKeyRel, ManyToManyRel
272
273        all_related_fields = self._get_fields(forward=False, reverse=True)
274        return make_immutable_fields_list(
275            "related_objects",
276            (
277                obj
278                for obj in all_related_fields
279                if isinstance(obj, ManyToManyRel | ForeignKeyRel)
280            ),
281        )
282
283    @cached_property
284    def _forward_fields_map(self) -> dict[str, Field]:
285        return {field.name: field for field in self._get_fields(reverse=False)}
286
287    @cached_property
288    def fields_map(self) -> dict[str, Field | ForeignObjectRel]:
289        return {
290            field.name: field for field in self._get_fields(forward=False, reverse=True)
291        }
292
293    def get_field(self, field_name: str) -> Field | ForeignObjectRel:
294        """
295        Return a field instance given the name of a forward or reverse field.
296        """
297        try:
298            # In order to avoid premature loading of the relation tree
299            # (expensive) we prefer checking if the field is a forward field.
300            return self._forward_fields_map[field_name]
301        except KeyError:
302            # If the app registry is not ready, reverse fields are
303            # unavailable, therefore we throw a FieldDoesNotExist exception.
304            if not self.models_registry.ready:
305                raise FieldDoesNotExist(
306                    f"{self.model} has no field named '{field_name}'. The app cache isn't ready yet, "
307                    "so if this is an auto-created related field, it won't "
308                    "be available yet."
309                )
310
311        try:
312            # Retrieve field instance by name from cached or just-computed
313            # field map.
314            return self.fields_map[field_name]
315        except KeyError:
316            raise FieldDoesNotExist(f"{self.model} has no field named '{field_name}'")
317
318    def get_forward_field(self, field_name: str) -> Field:
319        """
320        Return a forward field instance given the field name.
321
322        Raises FieldDoesNotExist if the field doesn't exist or is a reverse relation.
323        """
324        try:
325            return self._forward_fields_map[field_name]
326        except KeyError:
327            raise FieldDoesNotExist(
328                f"{self.model} has no forward field named '{field_name}'"
329            )
330
331    def get_reverse_relation(self, field_name: str) -> ForeignObjectRel:
332        """
333        Return a reverse relation instance given the field name.
334
335        Raises FieldDoesNotExist if the field doesn't exist or is a forward field.
336        """
337        # If the app registry is not ready, reverse fields are unavailable
338        if not self.models_registry.ready:
339            raise FieldDoesNotExist(
340                f"{self.model} has no reverse relation named '{field_name}'. The app cache isn't ready yet."
341            )
342
343        # Check if it's a forward field first
344        if field_name in self._forward_fields_map:
345            raise FieldDoesNotExist(
346                f"'{field_name}' is a forward field, not a reverse relation"
347            )
348
349        try:
350            return self.fields_map[field_name]  # ty: ignore[invalid-return-type]
351        except KeyError:
352            raise FieldDoesNotExist(
353                f"{self.model} has no reverse relation named '{field_name}'"
354            )
355
356    def _populate_directed_relation_graph(self) -> list[RelatedField]:
357        from plain.postgres.fields.related import RelatedField
358
359        """
360        This method is used by each model to find its reverse objects. As this
361        method is very expensive and is accessed frequently (it looks up every
362        field in a model, in every app), it is computed on first access and then
363        is set as a property on every model.
364        """
365        related_objects_graph: defaultdict[str, list[Any]] = defaultdict(list)
366
367        all_models = self.models_registry.get_models()
368        for model in all_models:
369            meta = model._model_meta
370
371            fields_with_relations = (
372                f
373                for f in meta._get_fields(reverse=False)
374                if isinstance(f, RelatedField)
375            )
376            for f in fields_with_relations:
377                if not isinstance(f.remote_field.model, str):
378                    remote_label = f.remote_field.model.model_options.label
379                    related_objects_graph[remote_label].append(f)
380
381        for model in all_models:
382            # Set the relation_tree using the internal __dict__. In this way
383            # we avoid calling the cached property. In attribute lookup,
384            # __dict__ takes precedence over a data descriptor (such as
385            # @cached_property). This means that the _model_meta._relation_tree is
386            # only called if related_objects is not in __dict__.
387            related_objects = related_objects_graph[model.model_options.label]
388            model._model_meta.__dict__["_relation_tree"] = related_objects
389        # It seems it is possible that self is not in all_models, so guard
390        # against that with default for get().
391        return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE)
392
393    @cached_property
394    def _relation_tree(self) -> list[RelatedField]:
395        return self._populate_directed_relation_graph()
396
397    def _expire_cache(self, forward: bool = True, reverse: bool = True) -> None:
398        # This method is usually called by packages.cache_clear(), when the
399        # registry is finalized, or when a new field is added.
400        if forward:
401            for cache_key in self.FORWARD_PROPERTIES:
402                if cache_key in self.__dict__:
403                    delattr(self, cache_key)
404        if reverse:
405            for cache_key in self.REVERSE_PROPERTIES:
406                if cache_key in self.__dict__:
407                    delattr(self, cache_key)
408        self._get_fields_cache = {}
409
410    @overload
411    def get_fields(
412        self, include_reverse: Literal[False] = False
413    ) -> ImmutableList[Field]: ...
414
415    @overload
416    def get_fields(
417        self, include_reverse: Literal[True]
418    ) -> ImmutableList[Field | ForeignObjectRel]: ...
419
420    def get_fields(
421        self, include_reverse: bool = False
422    ) -> ImmutableList[Field | ForeignObjectRel]:
423        """
424        Return a list of fields associated to the model.
425
426        By default, returns only forward fields (fields explicitly defined on
427        this model). Set include_reverse=True to also include reverse relations
428        (fields from other models that point to this model).
429
430        Args:
431            include_reverse: Include reverse relation fields (fields from other
432                           models pointing to this model). Needed for framework
433                           operations like migrations and deletion cascading.
434        """
435        return self._get_fields(reverse=include_reverse)
436
437    @overload
438    def _get_fields(
439        self,
440        *,
441        forward: Literal[True] = True,
442        reverse: Literal[False],
443        seen_models: set[type[Any]] | None = None,
444    ) -> ImmutableList[Field]: ...
445
446    @overload
447    def _get_fields(
448        self,
449        *,
450        forward: Literal[False],
451        reverse: Literal[True] = True,
452        seen_models: set[type[Any]] | None = None,
453    ) -> ImmutableList[ForeignObjectRel]: ...
454
455    @overload
456    def _get_fields(
457        self,
458        *,
459        forward: bool = True,
460        reverse: bool = True,
461        seen_models: set[type[Any]] | None = None,
462    ) -> ImmutableList[Field | ForeignObjectRel]: ...
463
464    def _get_fields(
465        self,
466        *,
467        forward: bool = True,
468        reverse: bool = True,
469        seen_models: set[type[Any]] | None = None,
470    ) -> ImmutableList[Field | ForeignObjectRel]:
471        """
472        Internal helper function to return fields of the model.
473
474        Args:
475            forward: If True, fields defined on this model are returned.
476            reverse: If True, reverse relations (fields from other models
477                    pointing to this model) are returned.
478            seen_models: Track visited models to prevent duplicates in recursion.
479        """
480
481        # This helper function is used to allow recursion in ``get_fields()``
482        # implementation and to provide a fast way for Plain's internals to
483        # access specific subsets of fields.
484
485        # We must keep track of which models we have already seen. Otherwise we
486        # could include the same field multiple times from different models.
487        topmost_call = seen_models is None
488        if seen_models is None:
489            seen_models = set()
490        seen_models.add(self.model)
491
492        # Creates a cache key composed of all arguments
493        cache_key = (forward, reverse, topmost_call)
494
495        try:
496            # In order to avoid list manipulation. Always return a shallow copy
497            # of the results.
498            return self._get_fields_cache[cache_key]
499        except KeyError:
500            pass
501
502        fields = []
503
504        if reverse:
505            # Tree is computed once and cached until the app cache is expired.
506            # It is composed of a list of fields from other models pointing to
507            # the current model (reverse relations).
508            all_fields = self._relation_tree
509            for field in all_fields:
510                fields.append(field.remote_field)
511
512        if forward:
513            # get_fields() intentionally returns a heterogeneous list of field types.
514            fields += self.local_fields  # ty: ignore[unsupported-operator]
515            fields += self.local_many_to_many  # ty: ignore[unsupported-operator]
516
517        # In order to avoid list manipulation. Always
518        # return a shallow copy of the results
519        fields = make_immutable_fields_list("get_fields()", fields)
520
521        # Store result into cache for later access
522        self._get_fields_cache[cache_key] = fields
523        return fields
524
525    @cached_property
526    def _property_names(self) -> frozenset[str]:
527        """Return a set of the names of the properties defined on the model."""
528        names = []
529        for name in dir(self.model):
530            attr = inspect.getattr_static(self.model, name)
531            if isinstance(attr, property):
532                names.append(name)
533        return frozenset(names)
534
535    @cached_property
536    def _non_pk_concrete_field_names(self) -> frozenset[str]:
537        """
538        Return a set of the non-primary key concrete field names defined on the model.
539        """
540        names = []
541        for field in self.concrete_fields:
542            if not field.primary_key:
543                names.append(field.name)
544        return frozenset(names)
545
546    @cached_property
547    def db_returning_fields(self) -> list[Field]:
548        """
549        Private API intended only to be used by Plain itself.
550        Fields to be returned after a database insert.
551        """
552        return [
553            field
554            for field in self._get_fields(forward=True, reverse=False)
555            if field.db_returning
556        ]
557
558    @property
559    def constraints_by_name(self) -> dict[str, BaseConstraint]:
560        """
561        Map each named constraint to its definition, keyed by the name
562        Postgres reports in ``err.diag.constraint_name`` — used on the write
563        path to translate an IntegrityError back to the constraint that raised
564        it.
565
566        A plain ``property``, not ``cached_property``: only read on the error
567        path, so recomputing is free, and it can never serve a stale map if
568        ``model_options.constraints`` is mutated.
569        """
570        return {
571            constraint.name: constraint
572            for constraint in self.model.model_options.constraints
573        }