plain.models 0.49.1__py3-none-any.whl → 0.50.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 (105) hide show
  1. plain/models/CHANGELOG.md +23 -0
  2. plain/models/aggregates.py +42 -19
  3. plain/models/backends/base/base.py +125 -105
  4. plain/models/backends/base/client.py +11 -3
  5. plain/models/backends/base/creation.py +22 -12
  6. plain/models/backends/base/features.py +10 -4
  7. plain/models/backends/base/introspection.py +29 -16
  8. plain/models/backends/base/operations.py +187 -91
  9. plain/models/backends/base/schema.py +267 -165
  10. plain/models/backends/base/validation.py +12 -3
  11. plain/models/backends/ddl_references.py +85 -43
  12. plain/models/backends/mysql/base.py +29 -26
  13. plain/models/backends/mysql/client.py +7 -2
  14. plain/models/backends/mysql/compiler.py +12 -3
  15. plain/models/backends/mysql/creation.py +5 -2
  16. plain/models/backends/mysql/features.py +24 -22
  17. plain/models/backends/mysql/introspection.py +22 -13
  18. plain/models/backends/mysql/operations.py +106 -39
  19. plain/models/backends/mysql/schema.py +48 -24
  20. plain/models/backends/mysql/validation.py +13 -6
  21. plain/models/backends/postgresql/base.py +41 -34
  22. plain/models/backends/postgresql/client.py +7 -2
  23. plain/models/backends/postgresql/creation.py +10 -5
  24. plain/models/backends/postgresql/introspection.py +15 -8
  25. plain/models/backends/postgresql/operations.py +109 -42
  26. plain/models/backends/postgresql/schema.py +85 -46
  27. plain/models/backends/sqlite3/_functions.py +151 -115
  28. plain/models/backends/sqlite3/base.py +37 -23
  29. plain/models/backends/sqlite3/client.py +7 -1
  30. plain/models/backends/sqlite3/creation.py +9 -5
  31. plain/models/backends/sqlite3/features.py +5 -3
  32. plain/models/backends/sqlite3/introspection.py +32 -16
  33. plain/models/backends/sqlite3/operations.py +125 -42
  34. plain/models/backends/sqlite3/schema.py +82 -58
  35. plain/models/backends/utils.py +52 -29
  36. plain/models/backups/cli.py +8 -6
  37. plain/models/backups/clients.py +16 -7
  38. plain/models/backups/core.py +24 -13
  39. plain/models/base.py +113 -74
  40. plain/models/cli.py +94 -63
  41. plain/models/config.py +1 -1
  42. plain/models/connections.py +23 -7
  43. plain/models/constraints.py +65 -47
  44. plain/models/database_url.py +1 -1
  45. plain/models/db.py +6 -2
  46. plain/models/deletion.py +66 -43
  47. plain/models/entrypoints.py +1 -1
  48. plain/models/enums.py +22 -11
  49. plain/models/exceptions.py +23 -8
  50. plain/models/expressions.py +440 -257
  51. plain/models/fields/__init__.py +253 -202
  52. plain/models/fields/json.py +120 -54
  53. plain/models/fields/mixins.py +12 -8
  54. plain/models/fields/related.py +284 -252
  55. plain/models/fields/related_descriptors.py +34 -25
  56. plain/models/fields/related_lookups.py +23 -11
  57. plain/models/fields/related_managers.py +81 -47
  58. plain/models/fields/reverse_related.py +58 -55
  59. plain/models/forms.py +89 -63
  60. plain/models/functions/comparison.py +71 -18
  61. plain/models/functions/datetime.py +79 -29
  62. plain/models/functions/math.py +43 -10
  63. plain/models/functions/mixins.py +24 -7
  64. plain/models/functions/text.py +104 -25
  65. plain/models/functions/window.py +12 -6
  66. plain/models/indexes.py +52 -28
  67. plain/models/lookups.py +228 -153
  68. plain/models/migrations/autodetector.py +86 -43
  69. plain/models/migrations/exceptions.py +7 -3
  70. plain/models/migrations/executor.py +33 -7
  71. plain/models/migrations/graph.py +79 -50
  72. plain/models/migrations/loader.py +45 -22
  73. plain/models/migrations/migration.py +23 -18
  74. plain/models/migrations/operations/base.py +37 -19
  75. plain/models/migrations/operations/fields.py +89 -42
  76. plain/models/migrations/operations/models.py +245 -143
  77. plain/models/migrations/operations/special.py +82 -25
  78. plain/models/migrations/optimizer.py +7 -2
  79. plain/models/migrations/questioner.py +58 -31
  80. plain/models/migrations/recorder.py +18 -11
  81. plain/models/migrations/serializer.py +50 -39
  82. plain/models/migrations/state.py +220 -133
  83. plain/models/migrations/utils.py +29 -13
  84. plain/models/migrations/writer.py +17 -14
  85. plain/models/options.py +63 -56
  86. plain/models/otel.py +16 -6
  87. plain/models/preflight.py +35 -12
  88. plain/models/query.py +323 -228
  89. plain/models/query_utils.py +93 -58
  90. plain/models/registry.py +34 -16
  91. plain/models/sql/compiler.py +146 -97
  92. plain/models/sql/datastructures.py +38 -25
  93. plain/models/sql/query.py +255 -169
  94. plain/models/sql/subqueries.py +32 -21
  95. plain/models/sql/where.py +54 -29
  96. plain/models/test/pytest.py +15 -11
  97. plain/models/test/utils.py +4 -2
  98. plain/models/transaction.py +20 -7
  99. plain/models/utils.py +13 -5
  100. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
  101. plain_models-0.50.0.dist-info/RECORD +122 -0
  102. plain_models-0.49.1.dist-info/RECORD +0 -122
  103. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
  104. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
  105. {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,16 +6,24 @@ 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.sql.compiler import SQLCompiler
26
+
19
27
  logger = logging.getLogger("plain.models")
20
28
 
21
29
  # PathInfo is used when converting lookups (fk__somecol). The contents
@@ -27,7 +35,7 @@ PathInfo = namedtuple(
27
35
  )
28
36
 
29
37
 
30
- def subclasses(cls):
38
+ def subclasses(cls: type) -> Generator[type, None, None]:
31
39
  yield cls
32
40
  for subclass in cls.__subclasses__():
33
41
  yield from subclasses(subclass)
@@ -46,14 +54,20 @@ class Q(tree.Node):
46
54
  default = AND
47
55
  conditional = True
48
56
 
49
- def __init__(self, *args, _connector=None, _negated=False, **kwargs):
57
+ def __init__(
58
+ self,
59
+ *args: Any,
60
+ _connector: str | None = None,
61
+ _negated: bool = False,
62
+ **kwargs: Any,
63
+ ) -> None:
50
64
  super().__init__(
51
65
  children=[*args, *sorted(kwargs.items())],
52
66
  connector=_connector,
53
67
  negated=_negated,
54
68
  )
55
69
 
56
- def _combine(self, other, conn):
70
+ def _combine(self, other: Any, conn: str) -> Q:
57
71
  if getattr(other, "conditional", False) is False:
58
72
  raise TypeError(other)
59
73
  if not self:
@@ -66,26 +80,31 @@ class Q(tree.Node):
66
80
  obj.add(other, conn)
67
81
  return obj
68
82
 
69
- def __or__(self, other):
83
+ def __or__(self, other: Any) -> Q:
70
84
  return self._combine(other, self.OR)
71
85
 
72
- def __and__(self, other):
86
+ def __and__(self, other: Any) -> Q:
73
87
  return self._combine(other, self.AND)
74
88
 
75
- def __xor__(self, other):
89
+ def __xor__(self, other: Any) -> Q:
76
90
  return self._combine(other, self.XOR)
77
91
 
78
- def __invert__(self):
92
+ def __invert__(self) -> Q:
79
93
  obj = self.copy()
80
94
  obj.negate()
81
95
  return obj
82
96
 
83
97
  def resolve_expression(
84
- self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False
85
- ):
98
+ self,
99
+ query: Any = None,
100
+ allow_joins: bool = True,
101
+ reuse: Any = None,
102
+ summarize: bool = False,
103
+ for_save: bool = False,
104
+ ) -> Any:
86
105
  # We must promote any new joins to left outer joins so that when Q is
87
106
  # used as an expression, rows aren't filtered due to joins.
88
- clause, joins = query._add_q(
107
+ clause, joins = query._add_q( # type: ignore[union-attr]
89
108
  self,
90
109
  reuse,
91
110
  allow_joins=allow_joins,
@@ -93,10 +112,10 @@ class Q(tree.Node):
93
112
  check_filterable=False,
94
113
  summarize=summarize,
95
114
  )
96
- query.promote_joins(joins)
115
+ query.promote_joins(joins) # type: ignore[union-attr]
97
116
  return clause
98
117
 
99
- def flatten(self):
118
+ def flatten(self) -> Generator[Any, None, None]:
100
119
  """
101
120
  Recursively yield this Q object and all subexpressions, in depth-first
102
121
  order.
@@ -111,7 +130,7 @@ class Q(tree.Node):
111
130
  else:
112
131
  yield child
113
132
 
114
- def check(self, against):
133
+ def check(self, against: dict[str, Any]) -> bool:
115
134
  """
116
135
  Do a database query to check if the expressions of the Q instance
117
136
  matches against the expressions.
@@ -123,7 +142,7 @@ class Q(tree.Node):
123
142
  from plain.models.sql import Query
124
143
  from plain.models.sql.constants import SINGLE
125
144
 
126
- query = Query(None)
145
+ query = Query(None) # type: ignore[arg-type]
127
146
  for name, value in against.items():
128
147
  if not hasattr(value, "resolve_expression"):
129
148
  value = Value(value)
@@ -141,12 +160,12 @@ class Q(tree.Node):
141
160
  logger.warning("Got a database error calling check() on %r: %s", self, e)
142
161
  return True
143
162
 
144
- def deconstruct(self):
163
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
145
164
  path = f"{self.__class__.__module__}.{self.__class__.__name__}"
146
165
  if path.startswith("plain.models.query_utils"):
147
166
  path = path.replace("plain.models.query_utils", "plain.models")
148
167
  args = tuple(self.children)
149
- kwargs = {}
168
+ kwargs: dict[str, Any] = {}
150
169
  if self.connector != self.default:
151
170
  kwargs["_connector"] = self.connector
152
171
  if self.negated:
@@ -160,10 +179,10 @@ class DeferredAttribute:
160
179
  object the first time, the query is executed.
161
180
  """
162
181
 
163
- def __init__(self, field):
182
+ def __init__(self, field: Any) -> None:
164
183
  self.field = field
165
184
 
166
- def __get__(self, instance, cls=None):
185
+ def __get__(self, instance: Any, cls: type | None = None) -> Any:
167
186
  """
168
187
  Retrieve and caches the value from the datastore on the first lookup.
169
188
  Return the cached value.
@@ -183,37 +202,37 @@ class class_or_instance_method:
183
202
  the caller type (instance or class of models.Field).
184
203
  """
185
204
 
186
- def __init__(self, class_method, instance_method):
205
+ def __init__(self, class_method: Any, instance_method: Any) -> None:
187
206
  self.class_method = class_method
188
207
  self.instance_method = instance_method
189
208
 
190
- def __get__(self, instance, owner):
209
+ def __get__(self, instance: Any, owner: type) -> Any:
191
210
  if instance is None:
192
211
  return functools.partial(self.class_method, owner)
193
212
  return functools.partial(self.instance_method, instance)
194
213
 
195
214
 
196
215
  class RegisterLookupMixin:
197
- def _get_lookup(self, lookup_name):
216
+ def _get_lookup(self, lookup_name: str) -> type | None:
198
217
  return self.get_lookups().get(lookup_name, None)
199
218
 
200
219
  @functools.cache
201
- def get_class_lookups(cls):
220
+ def get_class_lookups(cls: type) -> dict[str, type]:
202
221
  class_lookups = [
203
222
  parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
204
223
  ]
205
- return cls.merge_dicts(class_lookups)
224
+ return cls.merge_dicts(class_lookups) # type: ignore[attr-defined]
206
225
 
207
- def get_instance_lookups(self):
226
+ def get_instance_lookups(self) -> dict[str, type]:
208
227
  class_lookups = self.get_class_lookups()
209
228
  if instance_lookups := getattr(self, "instance_lookups", None):
210
229
  return {**class_lookups, **instance_lookups}
211
230
  return class_lookups
212
231
 
213
232
  get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
214
- get_class_lookups = classmethod(get_class_lookups)
233
+ get_class_lookups = classmethod(get_class_lookups) # type: ignore[assignment]
215
234
 
216
- def get_lookup(self, lookup_name):
235
+ def get_lookup(self, lookup_name: str) -> type | None:
217
236
  from plain.models.lookups import Lookup
218
237
 
219
238
  found = self._get_lookup(lookup_name)
@@ -223,7 +242,7 @@ class RegisterLookupMixin:
223
242
  return None
224
243
  return found
225
244
 
226
- def get_transform(self, lookup_name):
245
+ def get_transform(self, lookup_name: str) -> type | None:
227
246
  from plain.models.lookups import Transform
228
247
 
229
248
  found = self._get_lookup(lookup_name)
@@ -234,33 +253,37 @@ class RegisterLookupMixin:
234
253
  return found
235
254
 
236
255
  @staticmethod
237
- def merge_dicts(dicts):
256
+ def merge_dicts(dicts: list[dict[str, type]]) -> dict[str, type]:
238
257
  """
239
258
  Merge dicts in reverse to preference the order of the original list. e.g.,
240
259
  merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
241
260
  """
242
- merged = {}
261
+ merged: dict[str, type] = {}
243
262
  for d in reversed(dicts):
244
263
  merged.update(d)
245
264
  return merged
246
265
 
247
266
  @classmethod
248
- def _clear_cached_class_lookups(cls):
267
+ def _clear_cached_class_lookups(cls) -> None:
249
268
  for subclass in subclasses(cls):
250
- subclass.get_class_lookups.cache_clear()
269
+ subclass.get_class_lookups.cache_clear() # type: ignore[attr-defined]
251
270
 
252
- def register_class_lookup(cls, lookup, lookup_name=None):
271
+ def register_class_lookup(
272
+ cls: type, lookup: type, lookup_name: str | None = None
273
+ ) -> type:
253
274
  if lookup_name is None:
254
- lookup_name = lookup.lookup_name
275
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
255
276
  if "class_lookups" not in cls.__dict__:
256
- cls.class_lookups = {}
257
- cls.class_lookups[lookup_name] = lookup
258
- cls._clear_cached_class_lookups()
277
+ cls.class_lookups = {} # type: ignore[attr-defined]
278
+ cls.class_lookups[lookup_name] = lookup # type: ignore[attr-defined]
279
+ cls._clear_cached_class_lookups() # type: ignore[attr-defined]
259
280
  return lookup
260
281
 
261
- def register_instance_lookup(self, lookup, lookup_name=None):
282
+ def register_instance_lookup(
283
+ self, lookup: type, lookup_name: str | None = None
284
+ ) -> type:
262
285
  if lookup_name is None:
263
- lookup_name = lookup.lookup_name
286
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
264
287
  if "instance_lookups" not in self.__dict__:
265
288
  self.instance_lookups = {}
266
289
  self.instance_lookups[lookup_name] = lookup
@@ -269,34 +292,44 @@ class RegisterLookupMixin:
269
292
  register_lookup = class_or_instance_method(
270
293
  register_class_lookup, register_instance_lookup
271
294
  )
272
- register_class_lookup = classmethod(register_class_lookup)
295
+ register_class_lookup = classmethod(register_class_lookup) # type: ignore[assignment]
273
296
 
274
- def _unregister_class_lookup(cls, lookup, lookup_name=None):
297
+ def _unregister_class_lookup(
298
+ cls: type, lookup: type, lookup_name: str | None = None
299
+ ) -> None:
275
300
  """
276
301
  Remove given lookup from cls lookups. For use in tests only as it's
277
302
  not thread-safe.
278
303
  """
279
304
  if lookup_name is None:
280
- lookup_name = lookup.lookup_name
281
- del cls.class_lookups[lookup_name]
282
- cls._clear_cached_class_lookups()
305
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
306
+ del cls.class_lookups[lookup_name] # type: ignore[attr-defined]
307
+ cls._clear_cached_class_lookups() # type: ignore[attr-defined]
283
308
 
284
- def _unregister_instance_lookup(self, lookup, lookup_name=None):
309
+ def _unregister_instance_lookup(
310
+ self, lookup: type, lookup_name: str | None = None
311
+ ) -> None:
285
312
  """
286
313
  Remove given lookup from instance lookups. For use in tests only as
287
314
  it's not thread-safe.
288
315
  """
289
316
  if lookup_name is None:
290
- lookup_name = lookup.lookup_name
317
+ lookup_name = lookup.lookup_name # type: ignore[attr-defined]
291
318
  del self.instance_lookups[lookup_name]
292
319
 
293
320
  _unregister_lookup = class_or_instance_method(
294
321
  _unregister_class_lookup, _unregister_instance_lookup
295
322
  )
296
- _unregister_class_lookup = classmethod(_unregister_class_lookup)
323
+ _unregister_class_lookup = classmethod(_unregister_class_lookup) # type: ignore[assignment]
297
324
 
298
325
 
299
- def select_related_descend(field, restricted, requested, select_mask, reverse=False):
326
+ def select_related_descend(
327
+ field: Any,
328
+ restricted: bool,
329
+ requested: dict[str, Any],
330
+ select_mask: Any,
331
+ reverse: bool = False,
332
+ ) -> bool:
300
333
  """
301
334
  Return True if this field should be used to descend deeper for
302
335
  select_related() purposes. Used by both the query construction code
@@ -333,7 +366,9 @@ def select_related_descend(field, restricted, requested, select_mask, reverse=Fa
333
366
  return True
334
367
 
335
368
 
336
- def refs_expression(lookup_parts, annotations):
369
+ def refs_expression(
370
+ lookup_parts: list[str], annotations: dict[str, Any]
371
+ ) -> tuple[str | None, tuple[str, ...]]:
337
372
  """
338
373
  Check if the lookup_parts contains references to the given annotations set.
339
374
  Because the LOOKUP_SEP is contained in the default annotation names, check
@@ -342,11 +377,11 @@ def refs_expression(lookup_parts, annotations):
342
377
  for n in range(1, len(lookup_parts) + 1):
343
378
  level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
344
379
  if annotations.get(level_n_lookup):
345
- return level_n_lookup, lookup_parts[n:]
380
+ return level_n_lookup, tuple(lookup_parts[n:])
346
381
  return None, ()
347
382
 
348
383
 
349
- def check_rel_lookup_compatibility(model, target_opts, field):
384
+ def check_rel_lookup_compatibility(model: type, target_opts: Any, field: Any) -> bool:
350
385
  """
351
386
  Check that self.model is compatible with target_opts. Compatibility
352
387
  is OK if:
@@ -354,7 +389,7 @@ def check_rel_lookup_compatibility(model, target_opts, field):
354
389
  2) model is parent of opts' model or the other way around
355
390
  """
356
391
 
357
- def check(opts):
392
+ def check(opts: Any) -> bool:
358
393
  return model == opts.model
359
394
 
360
395
  # If the field is a primary key, then doing a query against the field's
@@ -374,17 +409,17 @@ def check_rel_lookup_compatibility(model, target_opts, field):
374
409
  class FilteredRelation:
375
410
  """Specify custom filtering in the ON clause of SQL joins."""
376
411
 
377
- def __init__(self, relation_name, *, condition=Q()):
412
+ def __init__(self, relation_name: str, *, condition: Q = Q()) -> None:
378
413
  if not relation_name:
379
414
  raise ValueError("relation_name cannot be empty.")
380
415
  self.relation_name = relation_name
381
- self.alias = None
416
+ self.alias: str | None = None
382
417
  if not isinstance(condition, Q):
383
418
  raise ValueError("condition argument must be a Q() instance.")
384
419
  self.condition = condition
385
- self.path = []
420
+ self.path: list[str] = []
386
421
 
387
- def __eq__(self, other):
422
+ def __eq__(self, other: object) -> bool:
388
423
  if not isinstance(other, self.__class__):
389
424
  return NotImplemented
390
425
  return (
@@ -393,20 +428,20 @@ class FilteredRelation:
393
428
  and self.condition == other.condition
394
429
  )
395
430
 
396
- def clone(self):
431
+ def clone(self) -> FilteredRelation:
397
432
  clone = FilteredRelation(self.relation_name, condition=self.condition)
398
433
  clone.alias = self.alias
399
434
  clone.path = self.path[:]
400
435
  return clone
401
436
 
402
- def resolve_expression(self, *args, **kwargs):
437
+ def resolve_expression(self, *args: Any, **kwargs: Any) -> Any:
403
438
  """
404
439
  QuerySet.annotate() only accepts expression-like arguments
405
440
  (with a resolve_expression() method).
406
441
  """
407
442
  raise NotImplementedError("FilteredRelation.resolve_expression() is unused.")
408
443
 
409
- def as_sql(self, compiler, connection):
444
+ def as_sql(self, compiler: SQLCompiler, connection: BaseDatabaseWrapper) -> Any:
410
445
  # Resolve the condition in Join.filtered_relation.
411
446
  query = compiler.query
412
447
  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,7 +103,7 @@ 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().
@@ -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
 
@@ -142,7 +158,9 @@ class ModelsRegistry:
142
158
  for model in package_models.values():
143
159
  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,7 +200,7 @@ 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().
@@ -196,7 +214,7 @@ 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):
217
+ def register_model(model_class: M) -> M:
200
218
  model_class._meta.models_registry.register_model(
201
219
  model_class._meta.package_label, model_class
202
220
  )