plain.models 0.49.2__py3-none-any.whl → 0.51.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 (108) hide show
  1. plain/models/CHANGELOG.md +27 -0
  2. plain/models/README.md +26 -42
  3. plain/models/__init__.py +2 -0
  4. plain/models/aggregates.py +42 -19
  5. plain/models/backends/base/base.py +125 -105
  6. plain/models/backends/base/client.py +11 -3
  7. plain/models/backends/base/creation.py +24 -14
  8. plain/models/backends/base/features.py +10 -4
  9. plain/models/backends/base/introspection.py +37 -20
  10. plain/models/backends/base/operations.py +187 -91
  11. plain/models/backends/base/schema.py +338 -218
  12. plain/models/backends/base/validation.py +13 -4
  13. plain/models/backends/ddl_references.py +85 -43
  14. plain/models/backends/mysql/base.py +29 -26
  15. plain/models/backends/mysql/client.py +7 -2
  16. plain/models/backends/mysql/compiler.py +13 -4
  17. plain/models/backends/mysql/creation.py +5 -2
  18. plain/models/backends/mysql/features.py +24 -22
  19. plain/models/backends/mysql/introspection.py +22 -13
  20. plain/models/backends/mysql/operations.py +107 -40
  21. plain/models/backends/mysql/schema.py +52 -28
  22. plain/models/backends/mysql/validation.py +13 -6
  23. plain/models/backends/postgresql/base.py +41 -34
  24. plain/models/backends/postgresql/client.py +7 -2
  25. plain/models/backends/postgresql/creation.py +10 -5
  26. plain/models/backends/postgresql/introspection.py +15 -8
  27. plain/models/backends/postgresql/operations.py +110 -43
  28. plain/models/backends/postgresql/schema.py +88 -49
  29. plain/models/backends/sqlite3/_functions.py +151 -115
  30. plain/models/backends/sqlite3/base.py +37 -23
  31. plain/models/backends/sqlite3/client.py +7 -1
  32. plain/models/backends/sqlite3/creation.py +9 -5
  33. plain/models/backends/sqlite3/features.py +5 -3
  34. plain/models/backends/sqlite3/introspection.py +32 -16
  35. plain/models/backends/sqlite3/operations.py +126 -43
  36. plain/models/backends/sqlite3/schema.py +127 -92
  37. plain/models/backends/utils.py +52 -29
  38. plain/models/backups/cli.py +8 -6
  39. plain/models/backups/clients.py +16 -7
  40. plain/models/backups/core.py +24 -13
  41. plain/models/base.py +221 -229
  42. plain/models/cli.py +98 -67
  43. plain/models/config.py +1 -1
  44. plain/models/connections.py +23 -7
  45. plain/models/constraints.py +79 -56
  46. plain/models/database_url.py +1 -1
  47. plain/models/db.py +6 -2
  48. plain/models/deletion.py +80 -56
  49. plain/models/entrypoints.py +1 -1
  50. plain/models/enums.py +22 -11
  51. plain/models/exceptions.py +23 -8
  52. plain/models/expressions.py +441 -258
  53. plain/models/fields/__init__.py +272 -217
  54. plain/models/fields/json.py +123 -57
  55. plain/models/fields/mixins.py +12 -8
  56. plain/models/fields/related.py +324 -290
  57. plain/models/fields/related_descriptors.py +33 -24
  58. plain/models/fields/related_lookups.py +24 -12
  59. plain/models/fields/related_managers.py +102 -79
  60. plain/models/fields/reverse_related.py +66 -63
  61. plain/models/forms.py +101 -75
  62. plain/models/functions/comparison.py +71 -18
  63. plain/models/functions/datetime.py +79 -29
  64. plain/models/functions/math.py +43 -10
  65. plain/models/functions/mixins.py +24 -7
  66. plain/models/functions/text.py +104 -25
  67. plain/models/functions/window.py +12 -6
  68. plain/models/indexes.py +57 -32
  69. plain/models/lookups.py +228 -153
  70. plain/models/meta.py +505 -0
  71. plain/models/migrations/autodetector.py +86 -43
  72. plain/models/migrations/exceptions.py +7 -3
  73. plain/models/migrations/executor.py +33 -7
  74. plain/models/migrations/graph.py +79 -50
  75. plain/models/migrations/loader.py +45 -22
  76. plain/models/migrations/migration.py +23 -18
  77. plain/models/migrations/operations/base.py +38 -20
  78. plain/models/migrations/operations/fields.py +95 -48
  79. plain/models/migrations/operations/models.py +246 -142
  80. plain/models/migrations/operations/special.py +82 -25
  81. plain/models/migrations/optimizer.py +7 -2
  82. plain/models/migrations/questioner.py +58 -31
  83. plain/models/migrations/recorder.py +27 -16
  84. plain/models/migrations/serializer.py +50 -39
  85. plain/models/migrations/state.py +232 -156
  86. plain/models/migrations/utils.py +30 -14
  87. plain/models/migrations/writer.py +17 -14
  88. plain/models/options.py +189 -518
  89. plain/models/otel.py +16 -6
  90. plain/models/preflight.py +42 -17
  91. plain/models/query.py +400 -251
  92. plain/models/query_utils.py +109 -69
  93. plain/models/registry.py +40 -21
  94. plain/models/sql/compiler.py +190 -127
  95. plain/models/sql/datastructures.py +38 -25
  96. plain/models/sql/query.py +320 -225
  97. plain/models/sql/subqueries.py +36 -25
  98. plain/models/sql/where.py +54 -29
  99. plain/models/test/pytest.py +15 -11
  100. plain/models/test/utils.py +4 -2
  101. plain/models/transaction.py +20 -7
  102. plain/models/utils.py +17 -6
  103. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
  104. plain_models-0.51.0.dist-info/RECORD +123 -0
  105. plain_models-0.49.2.dist-info/RECORD +0 -122
  106. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
  107. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
  108. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,28 +6,39 @@ large and/or so that they can be used by other modules without getting into
6
6
  circular import difficulties.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import functools
10
12
  import inspect
11
13
  import logging
12
14
  from collections import namedtuple
15
+ from collections.abc import Generator
16
+ from typing import TYPE_CHECKING, Any
13
17
 
14
18
  from plain.models.constants import LOOKUP_SEP
15
19
  from plain.models.db import DatabaseError, db_connection
16
20
  from plain.models.exceptions import FieldError
17
21
  from plain.utils import tree
18
22
 
23
+ if TYPE_CHECKING:
24
+ from plain.models.backends.base.base import BaseDatabaseWrapper
25
+ from plain.models.base import Model
26
+ from plain.models.fields import Field
27
+ from plain.models.meta import Meta
28
+ from plain.models.sql.compiler import SQLCompiler
29
+
19
30
  logger = logging.getLogger("plain.models")
20
31
 
21
32
  # PathInfo is used when converting lookups (fk__somecol). The contents
22
- # describe the relation in Model terms (model Options and Fields for both
23
- # sides of the relation. The join_field is the field backing the relation.
33
+ # describe the relation in Model terms (Meta and Fields for both
34
+ # sides of the relation). The join_field is the field backing the relation.
24
35
  PathInfo = namedtuple(
25
36
  "PathInfo",
26
- "from_opts to_opts target_fields join_field m2m direct filtered_relation",
37
+ "from_meta to_meta target_fields join_field m2m direct filtered_relation",
27
38
  )
28
39
 
29
40
 
30
- def subclasses(cls):
41
+ def subclasses(cls: type) -> Generator[type, None, None]:
31
42
  yield cls
32
43
  for subclass in cls.__subclasses__():
33
44
  yield from subclasses(subclass)
@@ -46,14 +57,20 @@ class Q(tree.Node):
46
57
  default = AND
47
58
  conditional = True
48
59
 
49
- def __init__(self, *args, _connector=None, _negated=False, **kwargs):
60
+ def __init__(
61
+ self,
62
+ *args: Any,
63
+ _connector: str | None = None,
64
+ _negated: bool = False,
65
+ **kwargs: Any,
66
+ ) -> None:
50
67
  super().__init__(
51
68
  children=[*args, *sorted(kwargs.items())],
52
69
  connector=_connector,
53
70
  negated=_negated,
54
71
  )
55
72
 
56
- def _combine(self, other, conn):
73
+ def _combine(self, other: Any, conn: str) -> Q:
57
74
  if getattr(other, "conditional", False) is False:
58
75
  raise TypeError(other)
59
76
  if not self:
@@ -66,26 +83,31 @@ class Q(tree.Node):
66
83
  obj.add(other, conn)
67
84
  return obj
68
85
 
69
- def __or__(self, other):
86
+ def __or__(self, other: Any) -> Q:
70
87
  return self._combine(other, self.OR)
71
88
 
72
- def __and__(self, other):
89
+ def __and__(self, other: Any) -> Q:
73
90
  return self._combine(other, self.AND)
74
91
 
75
- def __xor__(self, other):
92
+ def __xor__(self, other: Any) -> Q:
76
93
  return self._combine(other, self.XOR)
77
94
 
78
- def __invert__(self):
95
+ def __invert__(self) -> Q:
79
96
  obj = self.copy()
80
97
  obj.negate()
81
98
  return obj
82
99
 
83
100
  def resolve_expression(
84
- self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
85
- ):
101
+ self,
102
+ query: Any = None,
103
+ allow_joins: bool = True,
104
+ reuse: Any = None,
105
+ summarize: bool = False,
106
+ for_save: bool = False,
107
+ ) -> Any:
86
108
  # We must promote any new joins to left outer joins so that when Q is
87
109
  # used as an expression, rows aren't filtered due to joins.
88
- clause, joins = query._add_q(
110
+ clause, joins = query._add_q( # type: ignore[union-attr]
89
111
  self,
90
112
  reuse,
91
113
  allow_joins=allow_joins,
@@ -93,10 +115,10 @@ class Q(tree.Node):
93
115
  check_filterable=False,
94
116
  summarize=summarize,
95
117
  )
96
- query.promote_joins(joins)
118
+ query.promote_joins(joins) # type: ignore[union-attr]
97
119
  return clause
98
120
 
99
- def flatten(self):
121
+ def flatten(self) -> Generator[Any, None, None]:
100
122
  """
101
123
  Recursively yield this Q object and all subexpressions, in depth-first
102
124
  order.
@@ -111,7 +133,7 @@ class Q(tree.Node):
111
133
  else:
112
134
  yield child
113
135
 
114
- def check(self, against):
136
+ def check(self, against: dict[str, Any]) -> bool:
115
137
  """
116
138
  Do a database query to check if the expressions of the Q instance
117
139
  matches against the expressions.
@@ -123,7 +145,7 @@ class Q(tree.Node):
123
145
  from plain.models.sql import Query
124
146
  from plain.models.sql.constants import SINGLE
125
147
 
126
- query = Query(None)
148
+ query = Query(None) # type: ignore[arg-type]
127
149
  for name, value in against.items():
128
150
  if not hasattr(value, "resolve_expression"):
129
151
  value = Value(value)
@@ -141,12 +163,12 @@ class Q(tree.Node):
141
163
  logger.warning("Got a database error calling check() on %r: %s", self, e)
142
164
  return True
143
165
 
144
- def deconstruct(self):
166
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
145
167
  path = f"{self.__class__.__module__}.{self.__class__.__name__}"
146
168
  if path.startswith("plain.models.query_utils"):
147
169
  path = path.replace("plain.models.query_utils", "plain.models")
148
170
  args = tuple(self.children)
149
- kwargs = {}
171
+ kwargs: dict[str, Any] = {}
150
172
  if self.connector != self.default:
151
173
  kwargs["_connector"] = self.connector
152
174
  if self.negated:
@@ -160,10 +182,10 @@ class DeferredAttribute:
160
182
  object the first time, the query is executed.
161
183
  """
162
184
 
163
- def __init__(self, field):
185
+ def __init__(self, field: Any) -> None:
164
186
  self.field = field
165
187
 
166
- def __get__(self, instance, cls=None):
188
+ def __get__(self, instance: Any, cls: type | None = None) -> Any:
167
189
  """
168
190
  Retrieve and caches the value from the datastore on the first lookup.
169
191
  Return the cached value.
@@ -183,37 +205,37 @@ class class_or_instance_method:
183
205
  the caller type (instance or class of models.Field).
184
206
  """
185
207
 
186
- def __init__(self, class_method, instance_method):
208
+ def __init__(self, class_method: Any, instance_method: Any) -> None:
187
209
  self.class_method = class_method
188
210
  self.instance_method = instance_method
189
211
 
190
- def __get__(self, instance, owner):
212
+ def __get__(self, instance: Any, owner: type) -> Any:
191
213
  if instance is None:
192
214
  return functools.partial(self.class_method, owner)
193
215
  return functools.partial(self.instance_method, instance)
194
216
 
195
217
 
196
218
  class RegisterLookupMixin:
197
- def _get_lookup(self, lookup_name):
219
+ def _get_lookup(self, lookup_name: str) -> type | None:
198
220
  return self.get_lookups().get(lookup_name, None)
199
221
 
200
222
  @functools.cache
201
- def get_class_lookups(cls):
223
+ def get_class_lookups(cls: type) -> dict[str, type]:
202
224
  class_lookups = [
203
225
  parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
204
226
  ]
205
- return cls.merge_dicts(class_lookups)
227
+ return cls.merge_dicts(class_lookups) # type: ignore[attr-defined]
206
228
 
207
- def get_instance_lookups(self):
229
+ def get_instance_lookups(self) -> dict[str, type]:
208
230
  class_lookups = self.get_class_lookups()
209
231
  if instance_lookups := getattr(self, "instance_lookups", None):
210
232
  return {**class_lookups, **instance_lookups}
211
233
  return class_lookups
212
234
 
213
235
  get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
214
- get_class_lookups = classmethod(get_class_lookups)
236
+ get_class_lookups = classmethod(get_class_lookups) # type: ignore[assignment]
215
237
 
216
- def get_lookup(self, lookup_name):
238
+ def get_lookup(self, lookup_name: str) -> type | None:
217
239
  from plain.models.lookups import Lookup
218
240
 
219
241
  found = self._get_lookup(lookup_name)
@@ -223,7 +245,7 @@ class RegisterLookupMixin:
223
245
  return None
224
246
  return found
225
247
 
226
- def get_transform(self, lookup_name):
248
+ def get_transform(self, lookup_name: str) -> type | None:
227
249
  from plain.models.lookups import Transform
228
250
 
229
251
  found = self._get_lookup(lookup_name)
@@ -234,33 +256,37 @@ class RegisterLookupMixin:
234
256
  return found
235
257
 
236
258
  @staticmethod
237
- def merge_dicts(dicts):
259
+ def merge_dicts(dicts: list[dict[str, type]]) -> dict[str, type]:
238
260
  """
239
261
  Merge dicts in reverse to preference the order of the original list. e.g.,
240
262
  merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
241
263
  """
242
- merged = {}
264
+ merged: dict[str, type] = {}
243
265
  for d in reversed(dicts):
244
266
  merged.update(d)
245
267
  return merged
246
268
 
247
269
  @classmethod
248
- def _clear_cached_class_lookups(cls):
270
+ def _clear_cached_class_lookups(cls) -> None:
249
271
  for subclass in subclasses(cls):
250
- subclass.get_class_lookups.cache_clear()
272
+ subclass.get_class_lookups.cache_clear() # type: ignore[attr-defined]
251
273
 
252
- def register_class_lookup(cls, lookup, lookup_name=None):
274
+ def register_class_lookup(
275
+ cls: type, lookup: type, lookup_name: str | None = None
276
+ ) -> type:
253
277
  if lookup_name is None:
254
- lookup_name = lookup.lookup_name
278
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
255
279
  if "class_lookups" not in cls.__dict__:
256
- cls.class_lookups = {}
257
- cls.class_lookups[lookup_name] = lookup
258
- cls._clear_cached_class_lookups()
280
+ cls.class_lookups = {} # type: ignore[attr-defined]
281
+ cls.class_lookups[lookup_name] = lookup # type: ignore[attr-defined]
282
+ cls._clear_cached_class_lookups() # type: ignore[attr-defined]
259
283
  return lookup
260
284
 
261
- def register_instance_lookup(self, lookup, lookup_name=None):
285
+ def register_instance_lookup(
286
+ self, lookup: type, lookup_name: str | None = None
287
+ ) -> type:
262
288
  if lookup_name is None:
263
- lookup_name = lookup.lookup_name
289
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
264
290
  if "instance_lookups" not in self.__dict__:
265
291
  self.instance_lookups = {}
266
292
  self.instance_lookups[lookup_name] = lookup
@@ -269,34 +295,44 @@ class RegisterLookupMixin:
269
295
  register_lookup = class_or_instance_method(
270
296
  register_class_lookup, register_instance_lookup
271
297
  )
272
- register_class_lookup = classmethod(register_class_lookup)
298
+ register_class_lookup = classmethod(register_class_lookup) # type: ignore[assignment]
273
299
 
274
- def _unregister_class_lookup(cls, lookup, lookup_name=None):
300
+ def _unregister_class_lookup(
301
+ cls: type, lookup: type, lookup_name: str | None = None
302
+ ) -> None:
275
303
  """
276
304
  Remove given lookup from cls lookups. For use in tests only as it's
277
305
  not thread-safe.
278
306
  """
279
307
  if lookup_name is None:
280
- lookup_name = lookup.lookup_name
281
- del cls.class_lookups[lookup_name]
282
- cls._clear_cached_class_lookups()
308
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
309
+ del cls.class_lookups[lookup_name] # type: ignore[attr-defined]
310
+ cls._clear_cached_class_lookups() # type: ignore[attr-defined]
283
311
 
284
- def _unregister_instance_lookup(self, lookup, lookup_name=None):
312
+ def _unregister_instance_lookup(
313
+ self, lookup: type, lookup_name: str | None = None
314
+ ) -> None:
285
315
  """
286
316
  Remove given lookup from instance lookups. For use in tests only as
287
317
  it's not thread-safe.
288
318
  """
289
319
  if lookup_name is None:
290
- lookup_name = lookup.lookup_name
320
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
291
321
  del self.instance_lookups[lookup_name]
292
322
 
293
323
  _unregister_lookup = class_or_instance_method(
294
324
  _unregister_class_lookup, _unregister_instance_lookup
295
325
  )
296
- _unregister_class_lookup = classmethod(_unregister_class_lookup)
326
+ _unregister_class_lookup = classmethod(_unregister_class_lookup) # type: ignore[assignment]
297
327
 
298
328
 
299
- def select_related_descend(field, restricted, requested, select_mask, reverse=False):
329
+ def select_related_descend(
330
+ field: Any,
331
+ restricted: bool,
332
+ requested: dict[str, Any],
333
+ select_mask: Any,
334
+ reverse: bool = False,
335
+ ) -> bool:
300
336
  """
301
337
  Return True if this field should be used to descend deeper for
302
338
  select_related() purposes. Used by both the query construction code
@@ -327,13 +363,15 @@ def select_related_descend(field, restricted, requested, select_mask, reverse=Fa
327
363
  and field not in select_mask
328
364
  ):
329
365
  raise FieldError(
330
- f"Field {field.model._meta.object_name}.{field.name} cannot be both "
366
+ f"Field {field.model.model_options.object_name}.{field.name} cannot be both "
331
367
  "deferred and traversed using select_related at the same time."
332
368
  )
333
369
  return True
334
370
 
335
371
 
336
- def refs_expression(lookup_parts, annotations):
372
+ def refs_expression(
373
+ lookup_parts: list[str], annotations: dict[str, Any]
374
+ ) -> tuple[str | None, tuple[str, ...]]:
337
375
  """
338
376
  Check if the lookup_parts contains references to the given annotations set.
339
377
  Because the LOOKUP_SEP is contained in the default annotation names, check
@@ -342,20 +380,22 @@ def refs_expression(lookup_parts, annotations):
342
380
  for n in range(1, len(lookup_parts) + 1):
343
381
  level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
344
382
  if annotations.get(level_n_lookup):
345
- return level_n_lookup, lookup_parts[n:]
383
+ return level_n_lookup, tuple(lookup_parts[n:])
346
384
  return None, ()
347
385
 
348
386
 
349
- def check_rel_lookup_compatibility(model, target_opts, field):
387
+ def check_rel_lookup_compatibility(
388
+ model: type[Model], target_meta: Meta, field: Field
389
+ ) -> bool:
350
390
  """
351
- Check that self.model is compatible with target_opts. Compatibility
391
+ Check that model is compatible with target_meta. Compatibility
352
392
  is OK if:
353
- 1) model and opts match (where proxy inheritance is removed)
354
- 2) model is parent of opts' model or the other way around
393
+ 1) model and meta.model match (where proxy inheritance is removed)
394
+ 2) model is parent of meta's model or the other way around
355
395
  """
356
396
 
357
- def check(opts):
358
- return model == opts.model
397
+ def check(meta: Meta) -> bool:
398
+ return model == meta.model
359
399
 
360
400
  # If the field is a primary key, then doing a query against the field's
361
401
  # model is ok, too. Consider the case:
@@ -363,28 +403,28 @@ def check_rel_lookup_compatibility(model, target_opts, field):
363
403
  # place = OneToOneField(Place, primary_key=True):
364
404
  # Restaurant.query.filter(id__in=Restaurant.query.all()).
365
405
  # If we didn't have the primary key check, then id__in (== place__in) would
366
- # give Place's opts as the target opts, but Restaurant isn't compatible
406
+ # give Place's meta as the target meta, but Restaurant isn't compatible
367
407
  # with that. This logic applies only to primary keys, as when doing __in=qs,
368
408
  # we are going to turn this into __in=qs.values('id') later on.
369
- return check(target_opts) or (
370
- getattr(field, "primary_key", False) and check(field.model._meta)
409
+ return check(target_meta) or (
410
+ getattr(field, "primary_key", False) and check(field.model._model_meta)
371
411
  )
372
412
 
373
413
 
374
414
  class FilteredRelation:
375
415
  """Specify custom filtering in the ON clause of SQL joins."""
376
416
 
377
- def __init__(self, relation_name, *, condition=Q()):
417
+ def __init__(self, relation_name: str, *, condition: Q = Q()) -> None:
378
418
  if not relation_name:
379
419
  raise ValueError("relation_name cannot be empty.")
380
420
  self.relation_name = relation_name
381
- self.alias = None
421
+ self.alias: str | None = None
382
422
  if not isinstance(condition, Q):
383
423
  raise ValueError("condition argument must be a Q() instance.")
384
424
  self.condition = condition
385
- self.path = []
425
+ self.path: list[str] = []
386
426
 
387
- def __eq__(self, other):
427
+ def __eq__(self, other: object) -> bool:
388
428
  if not isinstance(other, self.__class__):
389
429
  return NotImplemented
390
430
  return (
@@ -393,20 +433,20 @@ class FilteredRelation:
393
433
  and self.condition == other.condition
394
434
  )
395
435
 
396
- def clone(self):
436
+ def clone(self) -> FilteredRelation:
397
437
  clone = FilteredRelation(self.relation_name, condition=self.condition)
398
438
  clone.alias = self.alias
399
439
  clone.path = self.path[:]
400
440
  return clone
401
441
 
402
- def resolve_expression(self, *args, **kwargs):
442
+ def resolve_expression(self, *args: Any, **kwargs: Any) -> Any:
403
443
  """
404
444
  QuerySet.annotate() only accepts expression-like arguments
405
445
  (with a resolve_expression() method).
406
446
  """
407
447
  raise NotImplementedError("FilteredRelation.resolve_expression() is unused.")
408
448
 
409
- def as_sql(self, compiler, connection):
449
+ def as_sql(self, compiler: SQLCompiler, connection: BaseDatabaseWrapper) -> Any:
410
450
  # Resolve the condition in Join.filtered_relation.
411
451
  query = compiler.query
412
452
  where = query.build_filtered_relation_q(self.condition, reuse=set(self.path))
plain/models/registry.py CHANGED
@@ -1,7 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import warnings
3
5
  from collections import defaultdict
6
+ from collections.abc import Callable
4
7
  from functools import partial
8
+ from typing import TYPE_CHECKING, TypeVar
9
+
10
+ if TYPE_CHECKING:
11
+ from plain.models.base import Model
12
+
13
+ M = TypeVar("M", bound="Model")
5
14
 
6
15
 
7
16
  class ModelsRegistryNotReady(Exception):
@@ -11,7 +20,7 @@ class ModelsRegistryNotReady(Exception):
11
20
 
12
21
 
13
22
  class ModelsRegistry:
14
- def __init__(self):
23
+ def __init__(self) -> None:
15
24
  # Mapping of app labels => model names => model classes. Every time a
16
25
  # model is imported, ModelBase.__new__ calls packages.register_model which
17
26
  # creates an entry in all_models. All imported models are registered,
@@ -19,23 +28,25 @@ class ModelsRegistry:
19
28
  # and whether the registry has been populated. Since it isn't possible
20
29
  # to reimport a module safely (it could reexecute initialization code)
21
30
  # all_models is never overridden or reset.
22
- self.all_models = defaultdict(dict)
31
+ self.all_models: defaultdict[str, dict[str, type[Model]]] = defaultdict(dict)
23
32
 
24
33
  # Maps ("package_label", "modelname") tuples to lists of functions to be
25
34
  # called when the corresponding model is ready. Used by this class's
26
35
  # `lazy_model_operation()` and `do_pending_operations()` methods.
27
- self._pending_operations = defaultdict(list)
36
+ self._pending_operations: defaultdict[
37
+ tuple[str, str], list[Callable[[type[Model]], None]]
38
+ ] = defaultdict(list)
28
39
 
29
- self.ready = False
40
+ self.ready: bool = False
30
41
 
31
- def check_ready(self):
42
+ def check_ready(self) -> None:
32
43
  """Raise an exception if all models haven't been imported yet."""
33
44
  if not self.ready:
34
45
  raise ModelsRegistryNotReady("Models aren't loaded yet.")
35
46
 
36
47
  # This method is performance-critical at least for Plain's test suite.
37
48
  @functools.cache
38
- def get_models(self, *, package_label=""):
49
+ def get_models(self, *, package_label: str = "") -> list[type[Model]]:
39
50
  """
40
51
  Return a list of all installed models.
41
52
 
@@ -65,7 +76,12 @@ class ModelsRegistry:
65
76
 
66
77
  return models
67
78
 
68
- def get_model(self, package_label, model_name=None, require_ready=True):
79
+ def get_model(
80
+ self,
81
+ package_label: str,
82
+ model_name: str | None = None,
83
+ require_ready: bool = True,
84
+ ) -> type[Model]:
69
85
  """
70
86
  Return the model matching the given package_label and model_name.
71
87
 
@@ -87,11 +103,11 @@ class ModelsRegistry:
87
103
  package_models = self.all_models[package_label]
88
104
  return package_models[model_name.lower()]
89
105
 
90
- def register_model(self, package_label, model):
106
+ def register_model(self, package_label: str, model: type[Model]) -> None:
91
107
  # Since this method is called when models are imported, it cannot
92
108
  # perform imports because of the risk of import loops. It mustn't
93
109
  # call get_package_config().
94
- model_name = model._meta.model_name
110
+ model_name = model.model_options.model_name
95
111
  app_models = self.all_models[package_label]
96
112
  if model_name in app_models:
97
113
  if (
@@ -113,7 +129,7 @@ class ModelsRegistry:
113
129
  self.do_pending_operations(model)
114
130
  self.clear_cache()
115
131
 
116
- def _get_registered_model(self, package_label, model_name):
132
+ def _get_registered_model(self, package_label: str, model_name: str) -> type[Model]:
117
133
  """
118
134
  Similar to get_model(), but doesn't require that an app exists with
119
135
  the given package_label.
@@ -126,7 +142,7 @@ class ModelsRegistry:
126
142
  raise LookupError(f"Model '{package_label}.{model_name}' not registered.")
127
143
  return model
128
144
 
129
- def clear_cache(self):
145
+ def clear_cache(self) -> None:
130
146
  """
131
147
  Clear all internal caches, for methods that alter the app registry.
132
148
 
@@ -140,9 +156,11 @@ class ModelsRegistry:
140
156
  # This particularly prevents that an empty value is cached while cloning.
141
157
  for package_models in self.all_models.values():
142
158
  for model in package_models.values():
143
- model._meta._expire_cache()
159
+ model._model_meta._expire_cache()
144
160
 
145
- def lazy_model_operation(self, function, *model_keys):
161
+ def lazy_model_operation(
162
+ self, function: Callable[..., None], *model_keys: tuple[str, str]
163
+ ) -> None:
146
164
  """
147
165
  Take a function and a number of ("package_label", "modelname") tuples, and
148
166
  when all the corresponding models have been imported and registered,
@@ -165,11 +183,11 @@ class ModelsRegistry:
165
183
  # This will be executed after the class corresponding to next_model
166
184
  # has been imported and registered. The `func` attribute provides
167
185
  # duck-type compatibility with partials.
168
- def apply_next_model(model):
169
- next_function = partial(apply_next_model.func, model)
186
+ def apply_next_model(model: type[Model]) -> None:
187
+ next_function = partial(apply_next_model.func, model) # type: ignore[attr-defined]
170
188
  self.lazy_model_operation(next_function, *more_models)
171
189
 
172
- apply_next_model.func = function
190
+ apply_next_model.func = function # type: ignore[attr-defined]
173
191
 
174
192
  # If the model has already been imported and registered, partially
175
193
  # apply it to the function now. If not, add it to the list of
@@ -182,12 +200,12 @@ class ModelsRegistry:
182
200
  else:
183
201
  apply_next_model(model_class)
184
202
 
185
- def do_pending_operations(self, model):
203
+ def do_pending_operations(self, model: type[Model]) -> None:
186
204
  """
187
205
  Take a newly-prepared model and pass it to each function waiting for
188
206
  it. This is called at the very end of Models.register_model().
189
207
  """
190
- key = model._meta.package_label, model._meta.model_name
208
+ key = model.model_options.package_label, model.model_options.model_name
191
209
  for function in self._pending_operations.pop(key, []):
192
210
  function(model)
193
211
 
@@ -196,8 +214,9 @@ models_registry = ModelsRegistry()
196
214
 
197
215
 
198
216
  # Decorator to register a model (using the internal registry for the correct state).
199
- def register_model(model_class):
200
- model_class._meta.models_registry.register_model(
201
- model_class._meta.package_label, model_class
217
+ def register_model(model_class: M) -> M:
218
+ model_class._model_meta.models_registry.register_model(
219
+ model_class.model_options.package_label,
220
+ model_class, # type: ignore[arg-type]
202
221
  )
203
222
  return model_class