plain.postgres 0.84.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. plain/postgres/CHANGELOG.md +1028 -0
  2. plain/postgres/README.md +925 -0
  3. plain/postgres/__init__.py +120 -0
  4. plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
  5. plain/postgres/aggregates.py +236 -0
  6. plain/postgres/backups/__init__.py +0 -0
  7. plain/postgres/backups/cli.py +148 -0
  8. plain/postgres/backups/clients.py +94 -0
  9. plain/postgres/backups/core.py +172 -0
  10. plain/postgres/base.py +1415 -0
  11. plain/postgres/cli/__init__.py +3 -0
  12. plain/postgres/cli/db.py +142 -0
  13. plain/postgres/cli/migrations.py +1085 -0
  14. plain/postgres/config.py +18 -0
  15. plain/postgres/connection.py +1331 -0
  16. plain/postgres/connections.py +77 -0
  17. plain/postgres/constants.py +13 -0
  18. plain/postgres/constraints.py +495 -0
  19. plain/postgres/database_url.py +94 -0
  20. plain/postgres/db.py +59 -0
  21. plain/postgres/default_settings.py +38 -0
  22. plain/postgres/deletion.py +475 -0
  23. plain/postgres/dialect.py +640 -0
  24. plain/postgres/entrypoints.py +4 -0
  25. plain/postgres/enums.py +103 -0
  26. plain/postgres/exceptions.py +217 -0
  27. plain/postgres/expressions.py +1912 -0
  28. plain/postgres/fields/__init__.py +2118 -0
  29. plain/postgres/fields/encrypted.py +354 -0
  30. plain/postgres/fields/json.py +413 -0
  31. plain/postgres/fields/mixins.py +30 -0
  32. plain/postgres/fields/related.py +1192 -0
  33. plain/postgres/fields/related_descriptors.py +290 -0
  34. plain/postgres/fields/related_lookups.py +223 -0
  35. plain/postgres/fields/related_managers.py +661 -0
  36. plain/postgres/fields/reverse_descriptors.py +229 -0
  37. plain/postgres/fields/reverse_related.py +328 -0
  38. plain/postgres/fields/timezones.py +143 -0
  39. plain/postgres/forms.py +773 -0
  40. plain/postgres/functions/__init__.py +189 -0
  41. plain/postgres/functions/comparison.py +127 -0
  42. plain/postgres/functions/datetime.py +454 -0
  43. plain/postgres/functions/math.py +140 -0
  44. plain/postgres/functions/mixins.py +59 -0
  45. plain/postgres/functions/text.py +282 -0
  46. plain/postgres/functions/window.py +125 -0
  47. plain/postgres/indexes.py +286 -0
  48. plain/postgres/lookups.py +758 -0
  49. plain/postgres/meta.py +584 -0
  50. plain/postgres/migrations/__init__.py +53 -0
  51. plain/postgres/migrations/autodetector.py +1379 -0
  52. plain/postgres/migrations/exceptions.py +54 -0
  53. plain/postgres/migrations/executor.py +188 -0
  54. plain/postgres/migrations/graph.py +364 -0
  55. plain/postgres/migrations/loader.py +377 -0
  56. plain/postgres/migrations/migration.py +180 -0
  57. plain/postgres/migrations/operations/__init__.py +34 -0
  58. plain/postgres/migrations/operations/base.py +139 -0
  59. plain/postgres/migrations/operations/fields.py +373 -0
  60. plain/postgres/migrations/operations/models.py +798 -0
  61. plain/postgres/migrations/operations/special.py +184 -0
  62. plain/postgres/migrations/optimizer.py +74 -0
  63. plain/postgres/migrations/questioner.py +340 -0
  64. plain/postgres/migrations/recorder.py +119 -0
  65. plain/postgres/migrations/serializer.py +378 -0
  66. plain/postgres/migrations/state.py +882 -0
  67. plain/postgres/migrations/utils.py +147 -0
  68. plain/postgres/migrations/writer.py +302 -0
  69. plain/postgres/options.py +207 -0
  70. plain/postgres/otel.py +231 -0
  71. plain/postgres/preflight.py +336 -0
  72. plain/postgres/query.py +2242 -0
  73. plain/postgres/query_utils.py +456 -0
  74. plain/postgres/registry.py +217 -0
  75. plain/postgres/schema.py +1885 -0
  76. plain/postgres/sql/__init__.py +40 -0
  77. plain/postgres/sql/compiler.py +1869 -0
  78. plain/postgres/sql/constants.py +22 -0
  79. plain/postgres/sql/datastructures.py +222 -0
  80. plain/postgres/sql/query.py +2947 -0
  81. plain/postgres/sql/where.py +374 -0
  82. plain/postgres/test/__init__.py +0 -0
  83. plain/postgres/test/pytest.py +117 -0
  84. plain/postgres/test/utils.py +18 -0
  85. plain/postgres/transaction.py +222 -0
  86. plain/postgres/types.py +92 -0
  87. plain/postgres/types.pyi +751 -0
  88. plain/postgres/utils.py +345 -0
  89. plain_postgres-0.84.0.dist-info/METADATA +937 -0
  90. plain_postgres-0.84.0.dist-info/RECORD +93 -0
  91. plain_postgres-0.84.0.dist-info/WHEEL +4 -0
  92. plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
  93. plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
@@ -0,0 +1,456 @@
1
+ """
2
+ Various data structures used in query construction.
3
+
4
+ Factored out from plain.postgres.query to avoid making the main module very
5
+ large and/or so that they can be used by other modules without getting into
6
+ circular import difficulties.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import functools
12
+ import inspect
13
+ import logging
14
+ from collections.abc import Callable, Generator
15
+ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Self
16
+
17
+ from plain.postgres.constants import LOOKUP_SEP
18
+ from plain.postgres.db import DatabaseError
19
+ from plain.postgres.exceptions import FieldError
20
+ from plain.utils import tree
21
+
22
+ if TYPE_CHECKING:
23
+ from plain.postgres.base import Model
24
+ from plain.postgres.connection import DatabaseConnection
25
+ from plain.postgres.fields import Field
26
+ from plain.postgres.fields.related import ForeignKeyField
27
+ from plain.postgres.fields.reverse_related import ForeignObjectRel
28
+ from plain.postgres.lookups import Lookup, Transform
29
+ from plain.postgres.meta import Meta
30
+ from plain.postgres.sql.compiler import SQLCompiler
31
+ from plain.postgres.sql.where import WhereNode
32
+
33
+ logger = logging.getLogger("plain.postgres")
34
+
35
+
36
+ class PathInfo(NamedTuple):
37
+ """Information about a relation path when converting lookups (fk__somecol).
38
+
39
+ Describes the relation in Model terms (Meta and Fields for both
40
+ sides of the relation). The join_field is the field backing the relation.
41
+ """
42
+
43
+ from_meta: Meta
44
+ to_meta: Meta
45
+ target_fields: tuple[Field, ...]
46
+ join_field: ForeignKeyField | ForeignObjectRel
47
+ m2m: bool
48
+ direct: bool
49
+ filtered_relation: FilteredRelation | None
50
+
51
+
52
+ def subclasses(cls: type) -> Generator[type]:
53
+ yield cls
54
+ for subclass in cls.__subclasses__():
55
+ yield from subclasses(subclass)
56
+
57
+
58
+ class Q(tree.Node):
59
+ """
60
+ Encapsulate filters as objects that can then be combined logically (using
61
+ `&` and `|`).
62
+ """
63
+
64
+ # Connection types
65
+ AND = "AND"
66
+ OR = "OR"
67
+ XOR = "XOR"
68
+ default = AND
69
+ conditional = True
70
+
71
+ def __init__(
72
+ self,
73
+ *args: Any,
74
+ _connector: str | None = None,
75
+ _negated: bool = False,
76
+ **kwargs: Any,
77
+ ) -> None:
78
+ super().__init__(
79
+ children=[*args, *sorted(kwargs.items())],
80
+ connector=_connector,
81
+ negated=_negated,
82
+ )
83
+
84
+ def _combine(self, other: Any, conn: str) -> Q:
85
+ if getattr(other, "conditional", False) is False:
86
+ raise TypeError(other)
87
+ if not self:
88
+ return other.copy()
89
+ if not other and isinstance(other, Q):
90
+ return self.copy()
91
+
92
+ obj = self.create(connector=conn)
93
+ obj.add(self, conn)
94
+ obj.add(other, conn)
95
+ return obj
96
+
97
+ def __or__(self, other: Any) -> Q:
98
+ return self._combine(other, self.OR)
99
+
100
+ def __and__(self, other: Any) -> Q:
101
+ return self._combine(other, self.AND)
102
+
103
+ def __xor__(self, other: Any) -> Q:
104
+ return self._combine(other, self.XOR)
105
+
106
+ def __invert__(self) -> Q:
107
+ obj = self.copy()
108
+ obj.negate()
109
+ return obj
110
+
111
+ def resolve_expression(
112
+ self,
113
+ query: Any = None,
114
+ allow_joins: bool = True,
115
+ reuse: Any = None,
116
+ summarize: bool = False,
117
+ for_save: bool = False,
118
+ ) -> WhereNode:
119
+ # We must promote any new joins to left outer joins so that when Q is
120
+ # used as an expression, rows aren't filtered due to joins.
121
+ clause, joins = query._add_q(
122
+ self,
123
+ reuse,
124
+ allow_joins=allow_joins,
125
+ split_subq=False,
126
+ check_filterable=False,
127
+ summarize=summarize,
128
+ )
129
+ query.promote_joins(joins)
130
+ return clause
131
+
132
+ def flatten(self) -> Generator[Any]:
133
+ """
134
+ Recursively yield this Q object and all subexpressions, in depth-first
135
+ order.
136
+ """
137
+ yield self
138
+ for child in self.children:
139
+ if isinstance(child, tuple):
140
+ # Use the lookup.
141
+ child = child[1]
142
+ if hasattr(child, "flatten"):
143
+ yield from child.flatten()
144
+ else:
145
+ yield child
146
+
147
+ def check(self, against: dict[str, Any]) -> bool:
148
+ """
149
+ Do a database query to check if the expressions of the Q instance
150
+ matches against the expressions.
151
+ """
152
+ # Avoid circular imports.
153
+ from plain.postgres.expressions import ResolvableExpression, Value
154
+ from plain.postgres.fields import BooleanField
155
+ from plain.postgres.functions import Coalesce
156
+ from plain.postgres.sql import SINGLE, Query
157
+
158
+ query = Query(None)
159
+ for name, value in against.items():
160
+ if not isinstance(value, ResolvableExpression):
161
+ value = Value(value)
162
+ query.add_annotation(value, name, select=False)
163
+ query.add_annotation(Value(1), "_check")
164
+ # This will raise a FieldError if a field is missing in "against".
165
+ query.add_q(Q(Coalesce(self, True, output_field=BooleanField())))
166
+ compiler = query.get_compiler()
167
+ try:
168
+ return compiler.execute_sql(SINGLE) is not None
169
+ except DatabaseError as e:
170
+ logger.warning("Got a database error calling check() on %r: %s", self, e)
171
+ return True
172
+
173
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
174
+ path = f"{self.__class__.__module__}.{self.__class__.__name__}"
175
+ if path.startswith("plain.postgres.query_utils"):
176
+ path = path.replace("plain.postgres.query_utils", "plain.postgres")
177
+ args = tuple(self.children)
178
+ kwargs: dict[str, Any] = {}
179
+ if self.connector != self.default:
180
+ kwargs["_connector"] = self.connector
181
+ if self.negated:
182
+ kwargs["_negated"] = True
183
+ return path, args, kwargs
184
+
185
+
186
+ class class_or_instance_method:
187
+ """
188
+ Hook used in RegisterLookupMixin to return partial functions depending on
189
+ the caller type (instance or class of models.Field).
190
+ """
191
+
192
+ def __init__(self, class_method: Any, instance_method: Any) -> None:
193
+ self.class_method = class_method
194
+ self.instance_method = instance_method
195
+
196
+ def __get__(self, instance: Any, owner: type) -> Any:
197
+ if instance is None:
198
+ return functools.partial(self.class_method, owner)
199
+ return functools.partial(self.instance_method, instance)
200
+
201
+
202
+ class RegisterLookupMixin:
203
+ class_lookups: ClassVar[dict[str, type[Lookup | Transform]]]
204
+
205
+ def _get_lookup(self, lookup_name: str) -> type[Lookup | Transform] | None:
206
+ return self.get_lookups().get(lookup_name, None)
207
+
208
+ @functools.cache
209
+ def get_class_lookups(cls: type[Self]) -> dict[str, type[Lookup | Transform]]:
210
+ class_lookups = [
211
+ parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
212
+ ]
213
+ return cls.merge_dicts(class_lookups)
214
+
215
+ def get_instance_lookups(self) -> dict[str, type[Lookup | Transform]]:
216
+ class_lookups = self.get_class_lookups()
217
+ if instance_lookups := getattr(self, "instance_lookups", None):
218
+ return {**class_lookups, **instance_lookups}
219
+ return class_lookups
220
+
221
+ get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
222
+ get_class_lookups: ClassVar[classmethod[Any, ..., Any]] = classmethod(
223
+ get_class_lookups
224
+ )
225
+
226
+ def get_lookup(self, lookup_name: str) -> type[Lookup] | None:
227
+ from plain.postgres.lookups import Lookup
228
+
229
+ found = self._get_lookup(lookup_name)
230
+ if found is None:
231
+ # output_field is a Field which inherits from RegisterLookupMixin
232
+ if output_field := getattr(self, "output_field", None):
233
+ return output_field.get_lookup(lookup_name)
234
+ if found is not None and not issubclass(found, Lookup):
235
+ return None
236
+ return found
237
+
238
+ def get_transform(
239
+ self, lookup_name: str
240
+ ) -> type[Transform] | Callable[..., Any] | None:
241
+ from plain.postgres.lookups import Transform
242
+
243
+ found = self._get_lookup(lookup_name)
244
+ if found is None:
245
+ # output_field is a Field which inherits from RegisterLookupMixin
246
+ if output_field := getattr(self, "output_field", None):
247
+ return output_field.get_transform(lookup_name)
248
+ if found is not None and not issubclass(found, Transform):
249
+ return None
250
+ return found
251
+
252
+ @staticmethod
253
+ def merge_dicts(
254
+ dicts: list[dict[str, type[Lookup | Transform]]],
255
+ ) -> dict[str, type[Lookup | Transform]]:
256
+ """
257
+ Merge dicts in reverse to preference the order of the original list. e.g.,
258
+ merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
259
+ """
260
+ merged: dict[str, type[Lookup | Transform]] = {}
261
+ for d in reversed(dicts):
262
+ merged.update(d)
263
+ return merged
264
+
265
+ @classmethod
266
+ def _clear_cached_class_lookups(cls: type[Self]) -> None:
267
+ for subclass in subclasses(cls):
268
+ if cached := getattr(subclass, "get_class_lookups", None):
269
+ cached.cache_clear()
270
+
271
+ def register_class_lookup(
272
+ cls: type[Self],
273
+ lookup: type[Lookup | Transform],
274
+ lookup_name: str | None = None,
275
+ ) -> type[Lookup | Transform]:
276
+ if lookup_name is None:
277
+ lookup_name = lookup.lookup_name
278
+ assert lookup_name is not None, "lookup_name must be set on the lookup class"
279
+ if "class_lookups" not in cls.__dict__:
280
+ cls.class_lookups = {}
281
+ cls.class_lookups[lookup_name] = lookup
282
+ cls._clear_cached_class_lookups()
283
+ return lookup
284
+
285
+ def register_instance_lookup(
286
+ self, lookup: type[Lookup | Transform], lookup_name: str | None = None
287
+ ) -> type[Lookup | Transform]:
288
+ if lookup_name is None:
289
+ lookup_name = lookup.lookup_name
290
+ if "instance_lookups" not in self.__dict__:
291
+ self.instance_lookups = {}
292
+ self.instance_lookups[lookup_name] = lookup
293
+ return lookup
294
+
295
+ register_lookup = class_or_instance_method(
296
+ register_class_lookup, register_instance_lookup
297
+ )
298
+ register_class_lookup: ClassVar[classmethod[Any, ..., Any]] = classmethod(
299
+ register_class_lookup
300
+ )
301
+
302
+ def _unregister_class_lookup(
303
+ cls: type[Self],
304
+ lookup: type[Lookup | Transform],
305
+ lookup_name: str | None = None,
306
+ ) -> None:
307
+ """
308
+ Remove given lookup from cls lookups. For use in tests only as it's
309
+ not thread-safe.
310
+ """
311
+ if lookup_name is None:
312
+ lookup_name = lookup.lookup_name
313
+ assert lookup_name is not None, "lookup_name must be set on the lookup class"
314
+ del cls.class_lookups[lookup_name]
315
+ cls._clear_cached_class_lookups()
316
+
317
+ def _unregister_instance_lookup(
318
+ self, lookup: type[Lookup | Transform], lookup_name: str | None = None
319
+ ) -> None:
320
+ """
321
+ Remove given lookup from instance lookups. For use in tests only as
322
+ it's not thread-safe.
323
+ """
324
+ if lookup_name is None:
325
+ lookup_name = lookup.lookup_name
326
+ del self.instance_lookups[lookup_name]
327
+
328
+ _unregister_lookup = class_or_instance_method(
329
+ _unregister_class_lookup, _unregister_instance_lookup
330
+ )
331
+ _unregister_class_lookup: ClassVar[classmethod[Any, ..., Any]] = classmethod(
332
+ _unregister_class_lookup
333
+ )
334
+
335
+
336
+ def select_related_descend(
337
+ field: Any,
338
+ restricted: bool | None,
339
+ requested: dict[str, Any] | None,
340
+ select_mask: Any,
341
+ reverse: bool = False,
342
+ ) -> bool:
343
+ """
344
+ Return True if this field should be used to descend deeper for
345
+ select_related() purposes. Used by both the query construction code
346
+ (compiler.get_related_selections()) and the model instance creation code
347
+ (compiler.klass_info).
348
+
349
+ Arguments:
350
+ * field - the field to be checked
351
+ * restricted - a boolean field, indicating if the field list has been
352
+ manually restricted using a requested clause)
353
+ * requested - The select_related() dictionary.
354
+ * select_mask - the dictionary of selected fields.
355
+ * reverse - boolean, True if we are checking a reverse select related
356
+ """
357
+ from plain.postgres.fields.related import RelatedField
358
+
359
+ if not isinstance(field, RelatedField):
360
+ return False
361
+ if restricted:
362
+ assert requested is not None, "requested must be provided when restricted=True"
363
+ if reverse and field.related_query_name() not in requested:
364
+ return False
365
+ if not reverse and field.name not in requested:
366
+ return False
367
+ if not restricted and field.allow_null:
368
+ return False
369
+ if (
370
+ restricted
371
+ and select_mask
372
+ and field.name in requested # type: ignore[operator]
373
+ and field not in select_mask
374
+ ):
375
+ raise FieldError(
376
+ f"Field {field.model.model_options.object_name}.{field.name} cannot be both "
377
+ "deferred and traversed using select_related at the same time."
378
+ )
379
+ return True
380
+
381
+
382
+ def refs_expression(
383
+ lookup_parts: list[str], annotations: dict[str, Any]
384
+ ) -> tuple[str | None, tuple[str, ...]]:
385
+ """
386
+ Check if the lookup_parts contains references to the given annotations set.
387
+ Because the LOOKUP_SEP is contained in the default annotation names, check
388
+ each prefix of the lookup_parts for a match.
389
+ """
390
+ for n in range(1, len(lookup_parts) + 1):
391
+ level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
392
+ if annotations.get(level_n_lookup):
393
+ return level_n_lookup, tuple(lookup_parts[n:])
394
+ return None, ()
395
+
396
+
397
+ def check_rel_lookup_compatibility(
398
+ model: type[Model], target_meta: Meta, field: Field | ForeignObjectRel
399
+ ) -> bool:
400
+ """
401
+ Check that model is compatible with target_meta. Compatibility
402
+ is OK if:
403
+ 1) model and meta.model match (where proxy inheritance is removed)
404
+ 2) model is parent of meta's model or the other way around
405
+ """
406
+
407
+ def check(meta: Meta) -> bool:
408
+ return model == meta.model
409
+
410
+ # If the field is a primary key, then doing a query against the field's
411
+ # model is ok, too. Consider the case:
412
+ # class Restaurant(models.Model):
413
+ # place = OneToOneField(Place, primary_key=True):
414
+ # Restaurant.query.filter(id__in=Restaurant.query.all()).
415
+ # If we didn't have the primary key check, then id__in (== place__in) would
416
+ # give Place's meta as the target meta, but Restaurant isn't compatible
417
+ # with that. This logic applies only to primary keys, as when doing __in=qs,
418
+ # we are going to turn this into __in=qs.values('id') later on.
419
+ return check(target_meta) or (
420
+ getattr(field, "primary_key", False) and check(field.model._model_meta)
421
+ )
422
+
423
+
424
+ class FilteredRelation:
425
+ """Specify custom filtering in the ON clause of SQL joins."""
426
+
427
+ def __init__(self, relation_name: str, *, condition: Q = Q()) -> None:
428
+ if not relation_name:
429
+ raise ValueError("relation_name cannot be empty.")
430
+ self.relation_name = relation_name
431
+ self.alias: str | None = None
432
+ if not isinstance(condition, Q):
433
+ raise ValueError("condition argument must be a Q() instance.")
434
+ self.condition = condition
435
+ self.path: list[str] = []
436
+
437
+ def __eq__(self, other: object) -> bool:
438
+ if not isinstance(other, self.__class__):
439
+ return NotImplemented
440
+ return (
441
+ self.relation_name == other.relation_name
442
+ and self.alias == other.alias
443
+ and self.condition == other.condition
444
+ )
445
+
446
+ def clone(self) -> FilteredRelation:
447
+ clone = FilteredRelation(self.relation_name, condition=self.condition)
448
+ clone.alias = self.alias
449
+ clone.path = self.path[:]
450
+ return clone
451
+
452
+ def as_sql(self, compiler: SQLCompiler, connection: DatabaseConnection) -> Any:
453
+ # Resolve the condition in Join.filtered_relation.
454
+ query = compiler.query
455
+ where = query.build_filtered_relation_q(self.condition, reuse=set(self.path))
456
+ return compiler.compile(where)
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import warnings
5
+ from collections import defaultdict
6
+ from collections.abc import Callable
7
+ from functools import partial
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from plain.postgres.base import Model
12
+
13
+
14
+ class ModelsRegistryNotReady(Exception):
15
+ """The plain.postgres registry is not populated yet"""
16
+
17
+ pass
18
+
19
+
20
+ class ModelsRegistry:
21
+ def __init__(self) -> None:
22
+ # Mapping of app labels => model names => model classes. Every time a
23
+ # model is imported, ModelBase.__new__ calls packages.register_model which
24
+ # creates an entry in all_models. All imported models are registered,
25
+ # regardless of whether they're defined in an installed application
26
+ # and whether the registry has been populated. Since it isn't possible
27
+ # to reimport a module safely (it could reexecute initialization code)
28
+ # all_models is never overridden or reset.
29
+ self.all_models: defaultdict[str, dict[str, type[Model]]] = defaultdict(dict)
30
+
31
+ # Maps ("package_label", "modelname") tuples to lists of functions to be
32
+ # called when the corresponding model is ready. Used by this class's
33
+ # `lazy_model_operation()` and `do_pending_operations()` methods.
34
+ self._pending_operations: defaultdict[
35
+ tuple[str, str], list[Callable[[type[Model]], None]]
36
+ ] = defaultdict(list)
37
+
38
+ self.ready: bool = False
39
+
40
+ def check_ready(self) -> None:
41
+ """Raise an exception if all models haven't been imported yet."""
42
+ if not self.ready:
43
+ raise ModelsRegistryNotReady("Models aren't loaded yet.")
44
+
45
+ # This method is performance-critical at least for Plain's test suite.
46
+ @functools.cache
47
+ def get_models(self, *, package_label: str = "") -> list[type[Model]]:
48
+ """
49
+ Return a list of all installed models.
50
+
51
+ By default, the following models aren't included:
52
+
53
+ - auto-created models for many-to-many relations without
54
+ an explicit intermediate table,
55
+
56
+ Set the corresponding keyword argument to True to include such models.
57
+ """
58
+
59
+ self.check_ready()
60
+
61
+ models = []
62
+
63
+ # Get models for a single package
64
+ if package_label:
65
+ package_models = self.all_models[package_label]
66
+ for model in package_models.values():
67
+ models.append(model)
68
+ return models
69
+
70
+ # Get models for all packages
71
+ for package_models in self.all_models.values():
72
+ for model in package_models.values():
73
+ models.append(model)
74
+
75
+ return models
76
+
77
+ def get_model(
78
+ self,
79
+ package_label: str,
80
+ model_name: str | None = None,
81
+ require_ready: bool = True,
82
+ ) -> type[Model]:
83
+ """
84
+ Return the model matching the given package_label and model_name.
85
+
86
+ As a shortcut, package_label may be in the form <package_label>.<model_name>.
87
+
88
+ model_name is case-insensitive.
89
+
90
+ Raise LookupError if no application exists with this label, or no
91
+ model exists with this name in the application. Raise ValueError if
92
+ called with a single argument that doesn't contain exactly one dot.
93
+ """
94
+
95
+ if require_ready:
96
+ self.check_ready()
97
+
98
+ if model_name is None:
99
+ package_label, model_name = package_label.split(".")
100
+
101
+ package_models = self.all_models[package_label]
102
+ return package_models[model_name.lower()]
103
+
104
+ def register_model(self, package_label: str, model: type[Model]) -> None:
105
+ # Since this method is called when models are imported, it cannot
106
+ # perform imports because of the risk of import loops. It mustn't
107
+ # call get_package_config().
108
+ model_name = model.model_options.model_name
109
+ app_models = self.all_models[package_label]
110
+ if model_name in app_models:
111
+ if (
112
+ model.__name__ == app_models[model_name].__name__
113
+ and model.__module__ == app_models[model_name].__module__
114
+ ):
115
+ warnings.warn(
116
+ f"Model '{package_label}.{model_name}' was already registered. Reloading models is not "
117
+ "advised as it can lead to inconsistencies, most notably with "
118
+ "related models.",
119
+ RuntimeWarning,
120
+ stacklevel=2,
121
+ )
122
+ else:
123
+ raise RuntimeError(
124
+ f"Conflicting '{model_name}' models in application '{package_label}': {app_models[model_name]} and {model}."
125
+ )
126
+ app_models[model_name] = model
127
+ self.do_pending_operations(model)
128
+ self.clear_cache()
129
+
130
+ def _get_registered_model(self, package_label: str, model_name: str) -> type[Model]:
131
+ """
132
+ Similar to get_model(), but doesn't require that an app exists with
133
+ the given package_label.
134
+
135
+ It's safe to call this method at import time, even while the registry
136
+ is being populated.
137
+ """
138
+ model = self.all_models[package_label].get(model_name.lower())
139
+ if model is None:
140
+ raise LookupError(f"Model '{package_label}.{model_name}' not registered.")
141
+ return model
142
+
143
+ def clear_cache(self) -> None:
144
+ """
145
+ Clear all internal caches, for methods that alter the app registry.
146
+
147
+ This is mostly used in tests.
148
+ """
149
+ # Call expire cache on each model. This will purge
150
+ # the relation tree and the fields cache.
151
+ self.get_models.cache_clear()
152
+ if self.ready:
153
+ # Circumvent self.get_models() to prevent that the cache is refilled.
154
+ # This particularly prevents that an empty value is cached while cloning.
155
+ for package_models in self.all_models.values():
156
+ for model in package_models.values():
157
+ model._model_meta._expire_cache()
158
+
159
+ def lazy_model_operation(
160
+ self, function: Callable[..., None], *model_keys: tuple[str, str]
161
+ ) -> None:
162
+ """
163
+ Take a function and a number of ("package_label", "modelname") tuples, and
164
+ when all the corresponding models have been imported and registered,
165
+ call the function with the model classes as its arguments.
166
+
167
+ The function passed to this method must accept exactly n models as
168
+ arguments, where n=len(model_keys).
169
+ """
170
+ # Base case: no arguments, just execute the function.
171
+ if not model_keys:
172
+ function()
173
+ # Recursive case: take the head of model_keys, wait for the
174
+ # corresponding model class to be imported and registered, then apply
175
+ # that argument to the supplied function. Pass the resulting partial
176
+ # to lazy_model_operation() along with the remaining model args and
177
+ # repeat until all models are loaded and all arguments are applied.
178
+ else:
179
+ next_model, *more_models = model_keys
180
+
181
+ # This will be executed after the class corresponding to next_model
182
+ # has been imported and registered.
183
+ def apply_next_model(model: type[Model]) -> None:
184
+ next_function = partial(function, model)
185
+ self.lazy_model_operation(next_function, *more_models)
186
+
187
+ # If the model has already been imported and registered, partially
188
+ # apply it to the function now. If not, add it to the list of
189
+ # pending operations for the model, where it will be executed with
190
+ # the model class as its sole argument once the model is ready.
191
+ try:
192
+ model_class = self._get_registered_model(*next_model)
193
+ except LookupError:
194
+ self._pending_operations[next_model].append(apply_next_model)
195
+ else:
196
+ apply_next_model(model_class)
197
+
198
+ def do_pending_operations(self, model: type[Model]) -> None:
199
+ """
200
+ Take a newly-prepared model and pass it to each function waiting for
201
+ it. This is called at the very end of Models.register_model().
202
+ """
203
+ key = model.model_options.package_label, model.model_options.model_name
204
+ for function in self._pending_operations.pop(key, []):
205
+ function(model)
206
+
207
+
208
+ models_registry = ModelsRegistry()
209
+
210
+
211
+ # Decorator to register a model (using the internal registry for the correct state).
212
+ def register_model[M: "Model"](model_class: type[M]) -> type[M]:
213
+ model_class._model_meta.models_registry.register_model(
214
+ model_class.model_options.package_label,
215
+ model_class,
216
+ )
217
+ return model_class