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
@@ -1,14 +1,17 @@
1
+ from __future__ import annotations
2
+
1
3
  import copy
2
4
  from collections import defaultdict
3
5
  from contextlib import contextmanager
4
6
  from functools import cached_property, partial
7
+ from typing import TYPE_CHECKING, Any
5
8
 
6
9
  from plain import models
7
10
  from plain.models.exceptions import FieldDoesNotExist
8
11
  from plain.models.fields import NOT_PROVIDED
9
12
  from plain.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
13
+ from plain.models.meta import Meta
10
14
  from plain.models.migrations.utils import field_is_referenced, get_references
11
- from plain.models.options import DEFAULT_NAMES
12
15
  from plain.models.registry import ModelsRegistry
13
16
  from plain.models.registry import models_registry as global_models
14
17
  from plain.packages import packages_registry
@@ -16,16 +19,23 @@ from plain.packages import packages_registry
16
19
  from .exceptions import InvalidBasesError
17
20
  from .utils import resolve_relation
18
21
 
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Generator, Iterable
24
+
25
+ from plain.models.fields import Field
26
+
19
27
 
20
- def _get_package_label_and_model_name(model, package_label=""):
28
+ def _get_package_label_and_model_name(
29
+ model: str | type[models.Model], package_label: str = ""
30
+ ) -> tuple[str, str]:
21
31
  if isinstance(model, str):
22
32
  split = model.split(".", 1)
23
- return tuple(split) if len(split) == 2 else (package_label, split[0])
33
+ return tuple(split) if len(split) == 2 else (package_label, split[0]) # type: ignore[return-value]
24
34
  else:
25
- return model._meta.package_label, model._meta.model_name
35
+ return model.model_options.package_label, model.model_options.model_name
26
36
 
27
37
 
28
- def _get_related_models(m):
38
+ def _get_related_models(m: type[models.Model]) -> list[type[models.Model]]:
29
39
  """Return all models that have a direct relationship to the given model."""
30
40
  related_models = [
31
41
  subclass
@@ -33,29 +43,29 @@ def _get_related_models(m):
33
43
  if issubclass(subclass, models.Model)
34
44
  ]
35
45
  related_fields_models = set()
36
- for f in m._meta.get_fields(include_hidden=True):
46
+ for f in m._model_meta.get_fields(include_hidden=True):
37
47
  if (
38
- f.is_relation
39
- and f.related_model is not None
40
- and not isinstance(f.related_model, str)
48
+ f.is_relation # type: ignore[attr-defined]
49
+ and f.related_model is not None # type: ignore[attr-defined]
50
+ and not isinstance(f.related_model, str) # type: ignore[attr-defined]
41
51
  ):
42
- related_fields_models.add(f.model)
43
- related_models.append(f.related_model)
52
+ related_fields_models.add(f.model) # type: ignore[attr-defined]
53
+ related_models.append(f.related_model) # type: ignore[attr-defined]
44
54
  return related_models
45
55
 
46
56
 
47
- def get_related_models_tuples(model):
57
+ def get_related_models_tuples(model: type[models.Model]) -> set[tuple[str, str]]:
48
58
  """
49
59
  Return a list of typical (package_label, model_name) tuples for all related
50
60
  models for the given model.
51
61
  """
52
62
  return {
53
- (rel_mod._meta.package_label, rel_mod._meta.model_name)
63
+ (rel_mod.model_options.package_label, rel_mod.model_options.model_name)
54
64
  for rel_mod in _get_related_models(model)
55
65
  }
56
66
 
57
67
 
58
- def get_related_models_recursive(model):
68
+ def get_related_models_recursive(model: type[models.Model]) -> set[tuple[str, str]]:
59
69
  """
60
70
  Return all models that have a direct or indirect relationship
61
71
  to the given model.
@@ -68,14 +78,14 @@ def get_related_models_recursive(model):
68
78
  queue = _get_related_models(model)
69
79
  for rel_mod in queue:
70
80
  rel_package_label, rel_model_name = (
71
- rel_mod._meta.package_label,
72
- rel_mod._meta.model_name,
81
+ rel_mod.model_options.package_label,
82
+ rel_mod.model_options.model_name,
73
83
  )
74
84
  if (rel_package_label, rel_model_name) in seen:
75
85
  continue
76
86
  seen.add((rel_package_label, rel_model_name))
77
87
  queue.extend(_get_related_models(rel_mod))
78
- return seen - {(model._meta.package_label, model._meta.model_name)}
88
+ return seen - {(model.model_options.package_label, model.model_options.model_name)}
79
89
 
80
90
 
81
91
  class ProjectState:
@@ -85,7 +95,11 @@ class ProjectState:
85
95
  FKs/etc. resolve properly.
86
96
  """
87
97
 
88
- def __init__(self, models=None, real_packages=None):
98
+ def __init__(
99
+ self,
100
+ models: dict[tuple[str, str], ModelState] | None = None,
101
+ real_packages: set[str] | None = None,
102
+ ):
89
103
  self.models = models or {}
90
104
  # Packages to include from main registry, usually unmigrated ones
91
105
  if real_packages is None:
@@ -95,15 +109,19 @@ class ProjectState:
95
109
  self.real_packages = real_packages
96
110
  self.is_delayed = False
97
111
  # {remote_model_key: {model_key: {field_name: field}}}
98
- self._relations = None
112
+ self._relations: (
113
+ dict[tuple[str, str], dict[tuple[str, str], dict[str, Field]]] | None
114
+ ) = None
99
115
 
100
116
  @property
101
- def relations(self):
117
+ def relations(
118
+ self,
119
+ ) -> dict[tuple[str, str], dict[tuple[str, str], dict[str, Field]]]:
102
120
  if self._relations is None:
103
121
  self.resolve_fields_and_relations()
104
- return self._relations
122
+ return self._relations # type: ignore[return-value]
105
123
 
106
- def add_model(self, model_state):
124
+ def add_model(self, model_state: ModelState) -> None:
107
125
  model_key = model_state.package_label, model_state.name_lower
108
126
  self.models[model_key] = model_state
109
127
  if self._relations is not None:
@@ -111,7 +129,7 @@ class ProjectState:
111
129
  if "models_registry" in self.__dict__: # hasattr would cache the property
112
130
  self.reload_model(*model_key)
113
131
 
114
- def remove_model(self, package_label, model_name):
132
+ def remove_model(self, package_label: str, model_name: str) -> None:
115
133
  model_key = package_label, model_name
116
134
  del self.models[model_key]
117
135
  if self._relations is not None:
@@ -127,7 +145,7 @@ class ProjectState:
127
145
  # the cache automatically (#24513)
128
146
  self.models_registry.clear_cache()
129
147
 
130
- def rename_model(self, package_label, old_name, new_name):
148
+ def rename_model(self, package_label: str, old_name: str, new_name: str) -> None:
131
149
  # Add a new model.
132
150
  old_name_lower = old_name.lower()
133
151
  new_name_lower = new_name.lower()
@@ -144,11 +162,11 @@ class ProjectState:
144
162
  changed_field = None
145
163
  if reference.to:
146
164
  changed_field = field.clone()
147
- changed_field.remote_field.model = new_remote_model
165
+ changed_field.remote_field.model = new_remote_model # type: ignore[attr-defined]
148
166
  if reference.through:
149
167
  if changed_field is None:
150
168
  changed_field = field.clone()
151
- changed_field.remote_field.through = new_remote_model
169
+ changed_field.remote_field.through = new_remote_model # type: ignore[attr-defined]
152
170
  if changed_field:
153
171
  model_state.fields[name] = changed_field
154
172
  to_reload.add((model_state.package_label, model_state.name_lower))
@@ -166,7 +184,13 @@ class ProjectState:
166
184
  self.remove_model(package_label, old_name_lower)
167
185
  self.reload_model(package_label, new_name_lower, delay=True)
168
186
 
169
- def alter_model_options(self, package_label, model_name, options, option_keys=None):
187
+ def alter_model_options(
188
+ self,
189
+ package_label: str,
190
+ model_name: str,
191
+ options: dict[str, Any],
192
+ option_keys: Iterable[str] | None = None,
193
+ ) -> None:
170
194
  model_state = self.models[package_label, model_name]
171
195
  model_state.options = {**model_state.options, **options}
172
196
  if option_keys:
@@ -175,48 +199,75 @@ class ProjectState:
175
199
  model_state.options.pop(key, False)
176
200
  self.reload_model(package_label, model_name, delay=True)
177
201
 
178
- def _append_option(self, package_label, model_name, option_name, obj):
202
+ def _append_option(
203
+ self, package_label: str, model_name: str, option_name: str, obj: Any
204
+ ) -> None:
179
205
  model_state = self.models[package_label, model_name]
180
206
  model_state.options[option_name] = [*model_state.options[option_name], obj]
181
207
  self.reload_model(package_label, model_name, delay=True)
182
208
 
183
- def _remove_option(self, package_label, model_name, option_name, obj_name):
209
+ def _remove_option(
210
+ self, package_label: str, model_name: str, option_name: str, obj_name: str
211
+ ) -> None:
184
212
  model_state = self.models[package_label, model_name]
185
213
  objs = model_state.options[option_name]
186
- model_state.options[option_name] = [obj for obj in objs if obj.name != obj_name]
214
+ model_state.options[option_name] = [
215
+ obj
216
+ for obj in objs
217
+ if obj.name != obj_name # type: ignore[attr-defined]
218
+ ]
187
219
  self.reload_model(package_label, model_name, delay=True)
188
220
 
189
- def add_index(self, package_label, model_name, index):
221
+ def add_index(self, package_label: str, model_name: str, index: Any) -> None:
190
222
  self._append_option(package_label, model_name, "indexes", index)
191
223
 
192
- def remove_index(self, package_label, model_name, index_name):
224
+ def remove_index(
225
+ self, package_label: str, model_name: str, index_name: str
226
+ ) -> None:
193
227
  self._remove_option(package_label, model_name, "indexes", index_name)
194
228
 
195
- def rename_index(self, package_label, model_name, old_index_name, new_index_name):
229
+ def rename_index(
230
+ self,
231
+ package_label: str,
232
+ model_name: str,
233
+ old_index_name: str,
234
+ new_index_name: str,
235
+ ) -> None:
196
236
  model_state = self.models[package_label, model_name]
197
237
  objs = model_state.options["indexes"]
198
238
 
199
239
  new_indexes = []
200
240
  for obj in objs:
201
- if obj.name == old_index_name:
202
- obj = obj.clone()
203
- obj.name = new_index_name
241
+ if obj.name == old_index_name: # type: ignore[attr-defined]
242
+ obj = obj.clone() # type: ignore[attr-defined]
243
+ obj.name = new_index_name # type: ignore[attr-defined]
204
244
  new_indexes.append(obj)
205
245
 
206
246
  model_state.options["indexes"] = new_indexes
207
247
  self.reload_model(package_label, model_name, delay=True)
208
248
 
209
- def add_constraint(self, package_label, model_name, constraint):
249
+ def add_constraint(
250
+ self, package_label: str, model_name: str, constraint: Any
251
+ ) -> None:
210
252
  self._append_option(package_label, model_name, "constraints", constraint)
211
253
 
212
- def remove_constraint(self, package_label, model_name, constraint_name):
254
+ def remove_constraint(
255
+ self, package_label: str, model_name: str, constraint_name: str
256
+ ) -> None:
213
257
  self._remove_option(package_label, model_name, "constraints", constraint_name)
214
258
 
215
- def add_field(self, package_label, model_name, name, field, preserve_default):
259
+ def add_field(
260
+ self,
261
+ package_label: str,
262
+ model_name: str,
263
+ name: str,
264
+ field: Field,
265
+ preserve_default: bool,
266
+ ) -> None:
216
267
  # If preserve default is off, don't use the default for future state.
217
268
  if not preserve_default:
218
269
  field = field.clone()
219
- field.default = NOT_PROVIDED
270
+ field.default = NOT_PROVIDED # type: ignore[attr-defined]
220
271
  else:
221
272
  field = field
222
273
  model_key = package_label, model_name
@@ -224,33 +275,40 @@ class ProjectState:
224
275
  if self._relations is not None:
225
276
  self.resolve_model_field_relations(model_key, name, field)
226
277
  # Delay rendering of relationships if it's not a relational field.
227
- delay = not field.is_relation
278
+ delay = not field.is_relation # type: ignore[attr-defined]
228
279
  self.reload_model(*model_key, delay=delay)
229
280
 
230
- def remove_field(self, package_label, model_name, name):
281
+ def remove_field(self, package_label: str, model_name: str, name: str) -> None:
231
282
  model_key = package_label, model_name
232
283
  model_state = self.models[model_key]
233
284
  old_field = model_state.fields.pop(name)
234
285
  if self._relations is not None:
235
286
  self.resolve_model_field_relations(model_key, name, old_field)
236
287
  # Delay rendering of relationships if it's not a relational field.
237
- delay = not old_field.is_relation
288
+ delay = not old_field.is_relation # type: ignore[attr-defined]
238
289
  self.reload_model(*model_key, delay=delay)
239
290
 
240
- def alter_field(self, package_label, model_name, name, field, preserve_default):
291
+ def alter_field(
292
+ self,
293
+ package_label: str,
294
+ model_name: str,
295
+ name: str,
296
+ field: Field,
297
+ preserve_default: bool,
298
+ ) -> None:
241
299
  if not preserve_default:
242
300
  field = field.clone()
243
- field.default = NOT_PROVIDED
301
+ field.default = NOT_PROVIDED # type: ignore[attr-defined]
244
302
  else:
245
303
  field = field
246
304
  model_key = package_label, model_name
247
305
  fields = self.models[model_key].fields
248
306
  if self._relations is not None:
249
307
  old_field = fields.pop(name)
250
- if old_field.is_relation:
308
+ if old_field.is_relation: # type: ignore[attr-defined]
251
309
  self.resolve_model_field_relations(model_key, name, old_field)
252
310
  fields[name] = field
253
- if field.is_relation:
311
+ if field.is_relation: # type: ignore[attr-defined]
254
312
  self.resolve_model_field_relations(model_key, name, field)
255
313
  else:
256
314
  fields[name] = field
@@ -258,12 +316,14 @@ class ProjectState:
258
316
  # it's sufficient if the new field is (#27737).
259
317
  # Delay rendering of relationships if it's not a relational field and
260
318
  # not referenced by a foreign key.
261
- delay = not field.is_relation and not field_is_referenced(
319
+ delay = not field.is_relation and not field_is_referenced( # type: ignore[attr-defined]
262
320
  self, model_key, (name, field)
263
321
  )
264
322
  self.reload_model(*model_key, delay=delay)
265
323
 
266
- def rename_field(self, package_label, model_name, old_name, new_name):
324
+ def rename_field(
325
+ self, package_label: str, model_name: str, old_name: str, new_name: str
326
+ ) -> None:
267
327
  model_key = package_label, model_name
268
328
  model_state = self.models[model_key]
269
329
  # Rename the field.
@@ -284,15 +344,17 @@ class ProjectState:
284
344
  for to_model in self._relations.values():
285
345
  if old_name_lower in to_model[model_key]:
286
346
  field = to_model[model_key].pop(old_name_lower)
287
- field.name = new_name_lower
347
+ field.name = new_name_lower # type: ignore[attr-defined]
288
348
  to_model[model_key][new_name_lower] = field
289
349
  self.reload_model(*model_key, delay=delay)
290
350
 
291
- def _find_reload_model(self, package_label, model_name, delay=False):
351
+ def _find_reload_model(
352
+ self, package_label: str, model_name: str, delay: bool = False
353
+ ) -> set[tuple[str, str]]:
292
354
  if delay:
293
355
  self.is_delayed = True
294
356
 
295
- related_models = set()
357
+ related_models: set[tuple[str, str]] = set()
296
358
 
297
359
  try:
298
360
  old_model = self.models_registry.get_model(package_label, model_name)
@@ -300,7 +362,7 @@ class ProjectState:
300
362
  pass
301
363
  else:
302
364
  # Get all relations to and from the old model before reloading,
303
- # as _meta.models_registry may change
365
+ # as _model_meta.models_registry may change
304
366
  if delay:
305
367
  related_models = get_related_models_tuples(old_model)
306
368
  else:
@@ -311,11 +373,12 @@ class ProjectState:
311
373
  # Directly related models are the models pointed to by ForeignKeys and ManyToManyFields.
312
374
  direct_related_models = set()
313
375
  for field in model_state.fields.values():
314
- if field.is_relation:
315
- if field.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT:
376
+ if field.is_relation: # type: ignore[attr-defined]
377
+ if field.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT: # type: ignore[attr-defined]
316
378
  continue
317
379
  rel_package_label, rel_model_name = _get_package_label_and_model_name(
318
- field.related_model, package_label
380
+ field.related_model,
381
+ package_label, # type: ignore[attr-defined]
319
382
  )
320
383
  direct_related_models.add((rel_package_label, rel_model_name.lower()))
321
384
 
@@ -339,12 +402,14 @@ class ProjectState:
339
402
 
340
403
  return related_models
341
404
 
342
- def reload_model(self, package_label, model_name, delay=False):
405
+ def reload_model(
406
+ self, package_label: str, model_name: str, delay: bool = False
407
+ ) -> None:
343
408
  if "models_registry" in self.__dict__: # hasattr would cache the property
344
409
  related_models = self._find_reload_model(package_label, model_name, delay)
345
410
  self._reload(related_models)
346
411
 
347
- def reload_models(self, models, delay=True):
412
+ def reload_models(self, models: set[tuple[str, str]], delay: bool = True) -> None:
348
413
  if "models_registry" in self.__dict__: # hasattr would cache the property
349
414
  related_models = set()
350
415
  for package_label, model_name in models:
@@ -353,7 +418,7 @@ class ProjectState:
353
418
  )
354
419
  self._reload(related_models)
355
420
 
356
- def _reload(self, related_models):
421
+ def _reload(self, related_models: set[tuple[str, str]]) -> None:
357
422
  # Unregister all related models
358
423
  with self.models_registry.bulk_update():
359
424
  for rel_package_label, rel_model_name in related_models:
@@ -381,19 +446,19 @@ class ProjectState:
381
446
 
382
447
  def update_model_field_relation(
383
448
  self,
384
- model,
385
- model_key,
386
- field_name,
387
- field,
388
- concretes,
389
- ):
449
+ model: str | type[models.Model],
450
+ model_key: tuple[str, str],
451
+ field_name: str,
452
+ field: Field,
453
+ concretes: dict[tuple[str, str], tuple[str, str]],
454
+ ) -> None:
390
455
  remote_model_key = resolve_relation(model, *model_key)
391
456
  if (
392
457
  remote_model_key[0] not in self.real_packages
393
458
  and remote_model_key in concretes
394
459
  ):
395
460
  remote_model_key = concretes[remote_model_key]
396
- relations_to_remote_model = self._relations[remote_model_key]
461
+ relations_to_remote_model = self._relations[remote_model_key] # type: ignore[index]
397
462
  if field_name in self.models[model_key].fields:
398
463
  # The assert holds because it's a new relation, or an altered
399
464
  # relation, in which case references have been removed by
@@ -407,19 +472,19 @@ class ProjectState:
407
472
 
408
473
  def resolve_model_field_relations(
409
474
  self,
410
- model_key,
411
- field_name,
412
- field,
413
- concretes=None,
414
- ):
415
- remote_field = field.remote_field
475
+ model_key: tuple[str, str],
476
+ field_name: str,
477
+ field: Field,
478
+ concretes: dict[tuple[str, str], tuple[str, str]] | None = None,
479
+ ) -> None:
480
+ remote_field = field.remote_field # type: ignore[attr-defined]
416
481
  if not remote_field:
417
- return
482
+ return None
418
483
  if concretes is None:
419
484
  concretes = self._get_concrete_models_mapping()
420
485
 
421
486
  self.update_model_field_relation(
422
- remote_field.model,
487
+ remote_field.model, # type: ignore[attr-defined]
423
488
  model_key,
424
489
  field_name,
425
490
  field,
@@ -428,12 +493,16 @@ class ProjectState:
428
493
 
429
494
  through = getattr(remote_field, "through", None)
430
495
  if not through:
431
- return
496
+ return None
432
497
  self.update_model_field_relation(
433
498
  through, model_key, field_name, field, concretes
434
499
  )
435
500
 
436
- def resolve_model_relations(self, model_key, concretes=None):
501
+ def resolve_model_relations(
502
+ self,
503
+ model_key: tuple[str, str],
504
+ concretes: dict[tuple[str, str], tuple[str, str]] | None = None,
505
+ ) -> None:
437
506
  if concretes is None:
438
507
  concretes = self._get_concrete_models_mapping()
439
508
 
@@ -441,11 +510,11 @@ class ProjectState:
441
510
  for field_name, field in model_state.fields.items():
442
511
  self.resolve_model_field_relations(model_key, field_name, field, concretes)
443
512
 
444
- def resolve_fields_and_relations(self):
513
+ def resolve_fields_and_relations(self) -> None:
445
514
  # Resolve fields.
446
515
  for model_state in self.models.values():
447
516
  for field_name, field in model_state.fields.items():
448
- field.name = field_name
517
+ field.name = field_name # type: ignore[attr-defined]
449
518
  # Resolve relations.
450
519
  # {remote_model_key: {model_key: {field_name: field}}}
451
520
  self._relations = defaultdict(partial(defaultdict, dict))
@@ -454,13 +523,13 @@ class ProjectState:
454
523
  for model_key in concretes:
455
524
  self.resolve_model_relations(model_key, concretes)
456
525
 
457
- def _get_concrete_models_mapping(self):
526
+ def _get_concrete_models_mapping(self) -> dict[tuple[str, str], tuple[str, str]]:
458
527
  concrete_models_mapping = {}
459
528
  for model_key, model_state in self.models.items():
460
529
  concrete_models_mapping[model_key] = model_key
461
530
  return concrete_models_mapping
462
531
 
463
- def clone(self):
532
+ def clone(self) -> ProjectState:
464
533
  """Return an exact copy of this ProjectState."""
465
534
  new_state = ProjectState(
466
535
  models={k: v.clone() for k, v in self.models.items()},
@@ -471,16 +540,16 @@ class ProjectState:
471
540
  new_state.is_delayed = self.is_delayed
472
541
  return new_state
473
542
 
474
- def clear_delayed_models_cache(self):
543
+ def clear_delayed_models_cache(self) -> None:
475
544
  if self.is_delayed and "models_registry" in self.__dict__:
476
545
  del self.__dict__["models_registry"]
477
546
 
478
547
  @cached_property
479
- def models_registry(self):
548
+ def models_registry(self) -> StateModelsRegistry:
480
549
  return StateModelsRegistry(self.real_packages, self.models)
481
550
 
482
551
  @classmethod
483
- def from_models_registry(cls, models_registry):
552
+ def from_models_registry(cls, models_registry: ModelsRegistry) -> ProjectState:
484
553
  """Take an Packages and return a ProjectState matching it."""
485
554
  app_models = {}
486
555
  for model in models_registry.get_models():
@@ -490,7 +559,9 @@ class ProjectState:
490
559
  )
491
560
  return cls(app_models)
492
561
 
493
- def __eq__(self, other):
562
+ def __eq__(self, other: object) -> bool:
563
+ if not isinstance(other, ProjectState):
564
+ return NotImplemented
494
565
  return self.models == other.models and self.real_packages == other.real_packages
495
566
 
496
567
 
@@ -500,18 +571,22 @@ class StateModelsRegistry(ModelsRegistry):
500
571
  additions and removals.
501
572
  """
502
573
 
503
- def __init__(self, real_packages, models):
574
+ def __init__(
575
+ self,
576
+ real_packages: set[str],
577
+ models: dict[tuple[str, str], ModelState],
578
+ ):
504
579
  # Any packages in self.real_packages should have all their models included
505
580
  # in the render. We don't use the original model instances as there
506
581
  # are some variables that refer to the Packages object.
507
582
  # FKs/M2Ms from real packages are also not included as they just
508
583
  # mess things up with partial states (due to lack of dependencies)
509
- self.real_models = []
584
+ self.real_models: list[ModelState] = []
510
585
  for package_label in real_packages:
511
586
  for model in global_models.get_models(package_label=package_label):
512
587
  self.real_models.append(ModelState.from_model(model, exclude_rels=True))
513
588
 
514
- super().__init__()
589
+ super().__init__() # type: ignore[misc]
515
590
 
516
591
  self.render_multiple([*models.values(), *self.real_models])
517
592
 
@@ -521,10 +596,10 @@ class StateModelsRegistry(ModelsRegistry):
521
596
  from plain.models.preflight import _check_lazy_references
522
597
 
523
598
  if errors := _check_lazy_references(self, packages_registry):
524
- raise ValueError("\n".join(error.message for error in errors))
599
+ raise ValueError("\n".join(error.message for error in errors)) # type: ignore[attr-defined]
525
600
 
526
601
  @contextmanager
527
- def bulk_update(self):
602
+ def bulk_update(self) -> Generator[None, None, None]:
528
603
  # Avoid clearing each model's cache for each change. Instead, clear
529
604
  # all caches when we're finished updating the model instances.
530
605
  ready = self.ready
@@ -535,13 +610,13 @@ class StateModelsRegistry(ModelsRegistry):
535
610
  self.ready = ready
536
611
  self.clear_cache()
537
612
 
538
- def render_multiple(self, model_states):
613
+ def render_multiple(self, model_states: list[ModelState]) -> None:
539
614
  # We keep trying to render the models in a loop, ignoring invalid
540
615
  # base errors, until the size of the unrendered models doesn't
541
616
  # decrease by at least one, meaning there's a base dependency loop/
542
617
  # missing base.
543
618
  if not model_states:
544
- return
619
+ return None
545
620
  # Prevent that all model caches are expired for each render.
546
621
  with self.bulk_update():
547
622
  unrendered_models = model_states
@@ -560,21 +635,21 @@ class StateModelsRegistry(ModelsRegistry):
560
635
  )
561
636
  unrendered_models = new_unrendered_models
562
637
 
563
- def clone(self):
638
+ def clone(self) -> StateModelsRegistry:
564
639
  """Return a clone of this registry."""
565
- clone = StateModelsRegistry([], {})
640
+ clone = StateModelsRegistry(set(), {})
566
641
  clone.all_models = copy.deepcopy(self.all_models)
567
642
 
568
643
  # No need to actually clone them, they'll never change
569
644
  clone.real_models = self.real_models
570
645
  return clone
571
646
 
572
- def register_model(self, package_label, model):
573
- self.all_models[package_label][model._meta.model_name] = model
647
+ def register_model(self, package_label: str, model: type[models.Model]) -> None:
648
+ self.all_models[package_label][model.model_options.model_name] = model
574
649
  self.do_pending_operations(model)
575
650
  self.clear_cache()
576
651
 
577
- def unregister_model(self, package_label, model_name):
652
+ def unregister_model(self, package_label: str, model_name: str) -> None:
578
653
  try:
579
654
  del self.all_models[package_label][model_name]
580
655
  except KeyError:
@@ -592,10 +667,17 @@ class ModelState:
592
667
  assign new ones, as these are not detached during a clone.
593
668
  """
594
669
 
595
- def __init__(self, package_label, name, fields, options=None, bases=None):
670
+ def __init__(
671
+ self,
672
+ package_label: str,
673
+ name: str,
674
+ fields: Iterable[tuple[str, Field]],
675
+ options: dict[str, Any] | None = None,
676
+ bases: tuple[str | type[models.Model], ...] | None = None,
677
+ ):
596
678
  self.package_label = package_label
597
679
  self.name = name
598
- self.fields = dict(fields)
680
+ self.fields: dict[str, Field] = dict(fields)
599
681
  self.options = options or {}
600
682
  self.options.setdefault("indexes", [])
601
683
  self.options.setdefault("constraints", [])
@@ -607,79 +689,64 @@ class ModelState:
607
689
  f'ModelState.fields cannot be bound to a model - "{name}" is.'
608
690
  )
609
691
  # Sanity-check that relation fields are NOT referring to a model class.
610
- if field.is_relation and hasattr(field.related_model, "_meta"):
692
+ if field.is_relation and hasattr(field.related_model, "_model_meta"):
611
693
  raise ValueError(
612
694
  f'ModelState.fields cannot refer to a model class - "{name}.to" does. '
613
695
  "Use a string reference instead."
614
696
  )
615
- if field.many_to_many and hasattr(field.remote_field.through, "_meta"):
697
+ if field.many_to_many and hasattr(
698
+ field.remote_field.through, "_model_meta"
699
+ ):
616
700
  raise ValueError(
617
701
  f'ModelState.fields cannot refer to a model class - "{name}.through" '
618
702
  "does. Use a string reference instead."
619
703
  )
620
704
  # Sanity-check that indexes have their name set.
621
705
  for index in self.options["indexes"]:
622
- if not index.name:
706
+ if not index.name: # type: ignore[attr-defined]
623
707
  raise ValueError(
624
708
  "Indexes passed to ModelState require a name attribute. "
625
709
  f"{index!r} doesn't have one."
626
710
  )
627
711
 
628
712
  @cached_property
629
- def name_lower(self):
713
+ def name_lower(self) -> str:
630
714
  return self.name.lower()
631
715
 
632
- def get_field(self, field_name):
716
+ def get_field(self, field_name: str) -> Field:
633
717
  return self.fields[field_name]
634
718
 
635
719
  @classmethod
636
- def from_model(cls, model, exclude_rels=False):
720
+ def from_model(
721
+ cls, model: type[models.Model], exclude_rels: bool = False
722
+ ) -> ModelState:
637
723
  """Given a model, return a ModelState representing it."""
638
724
  # Deconstruct the fields
639
725
  fields = []
640
- for field in model._meta.local_fields:
726
+ for field in model._model_meta.local_fields:
641
727
  if getattr(field, "remote_field", None) and exclude_rels:
642
728
  continue
643
- name = field.name
729
+ name = field.name # type: ignore[attr-defined]
644
730
  try:
645
- fields.append((name, field.clone()))
731
+ fields.append((name, field.clone())) # type: ignore[attr-defined]
646
732
  except TypeError as e:
647
733
  raise TypeError(
648
- f"Couldn't reconstruct field {name} on {model._meta.label}: {e}"
734
+ f"Couldn't reconstruct field {name} on {model.model_options.label}: {e}"
649
735
  )
650
736
  if not exclude_rels:
651
- for field in model._meta.local_many_to_many:
652
- name = field.name
737
+ for field in model._model_meta.local_many_to_many:
738
+ name = field.name # type: ignore[attr-defined]
653
739
  try:
654
- fields.append((name, field.clone()))
740
+ fields.append((name, field.clone())) # type: ignore[attr-defined]
655
741
  except TypeError as e:
656
742
  raise TypeError(
657
- f"Couldn't reconstruct m2m field {name} on {model._meta.object_name}: {e}"
743
+ f"Couldn't reconstruct m2m field {name} on {model.model_options.object_name}: {e}"
658
744
  )
659
- # Extract the options
660
- options = {}
661
- for name in DEFAULT_NAMES:
662
- # Ignore some special options
663
- if name in ["models_registry", "package_label"]:
664
- continue
665
- elif name in model._meta.original_attrs:
666
- if name == "indexes":
667
- indexes = [idx.clone() for idx in model._meta.indexes]
668
- for index in indexes:
669
- if not index.name:
670
- index.set_name_with_model(model)
671
- options["indexes"] = indexes
672
- elif name == "constraints":
673
- options["constraints"] = [
674
- con.clone() for con in model._meta.constraints
675
- ]
676
- else:
677
- options[name] = model._meta.original_attrs[name]
678
745
 
679
- def flatten_bases(model):
746
+ def flatten_bases(model: type[models.Model]) -> list[type[models.Model]]:
680
747
  bases = []
681
748
  for base in model.__bases__:
682
- bases.append(base)
749
+ bases.append(base) # type: ignore[arg-type]
683
750
  return bases
684
751
 
685
752
  # We can't rely on __mro__ directly because we only want to flatten
@@ -693,7 +760,13 @@ class ModelState:
693
760
 
694
761
  # Make our record
695
762
  bases = tuple(
696
- (base._meta.label_lower if hasattr(base, "_meta") else base)
763
+ (
764
+ base.model_options.label_lower
765
+ if not isinstance(base, str)
766
+ and base is not models.Model
767
+ and hasattr(base, "_model_meta")
768
+ else base
769
+ )
697
770
  for base in flattened_bases
698
771
  )
699
772
  # Ensure at least one base inherits from models.Model
@@ -704,14 +777,14 @@ class ModelState:
704
777
 
705
778
  # Construct the new ModelState
706
779
  return cls(
707
- model._meta.package_label,
708
- model._meta.object_name,
780
+ model.model_options.package_label,
781
+ model.model_options.object_name,
709
782
  fields,
710
- options,
783
+ model.model_options.export_for_migrations(),
711
784
  bases,
712
785
  )
713
786
 
714
- def clone(self):
787
+ def clone(self) -> ModelState:
715
788
  """Return an exact copy of this ModelState."""
716
789
  return self.__class__(
717
790
  package_label=self.package_label,
@@ -724,15 +797,13 @@ class ModelState:
724
797
  bases=self.bases,
725
798
  )
726
799
 
727
- def render(self, models_registry):
800
+ def render(self, models_registry: ModelsRegistry) -> type[models.Model]:
728
801
  """Create a Model object from our current state into the given packages."""
729
- # First, make a Meta object
730
- meta_contents = {
731
- "package_label": self.package_label,
732
- "models_registry": models_registry,
802
+ # Create Options instance with metadata
803
+ meta_options = models.Options(
804
+ package_label=self.package_label,
733
805
  **self.options,
734
- }
735
- meta = type("Meta", (), meta_contents)
806
+ )
736
807
  # Then, work out our bases
737
808
  try:
738
809
  bases = tuple(
@@ -744,8 +815,11 @@ class ModelState:
744
815
  f"Cannot resolve one or more bases from {self.bases!r}"
745
816
  )
746
817
  # Clone fields for the body, add other bits.
747
- body = {name: field.clone() for name, field in self.fields.items()}
748
- body["Meta"] = meta
818
+ body = {name: field.clone() for name, field in self.fields.items()} # type: ignore[attr-defined]
819
+ body["model_options"] = meta_options
820
+ body["_model_meta"] = Meta(
821
+ models_registry=models_registry
822
+ ) # Use custom registry
749
823
  body["__module__"] = "__fake__"
750
824
 
751
825
  # Then, make a Model object (models_registry.register_model is called in __new__)
@@ -754,32 +828,34 @@ class ModelState:
754
828
 
755
829
  # Register it to the models_registry associated with the model meta
756
830
  # (could probably do this directly right here too...)
757
- register_model(model_class)
831
+ register_model(model_class) # type: ignore[arg-type]
758
832
 
759
- return model_class
833
+ return model_class # type: ignore[return-value]
760
834
 
761
- def get_index_by_name(self, name):
835
+ def get_index_by_name(self, name: str) -> Any:
762
836
  for index in self.options["indexes"]:
763
- if index.name == name:
837
+ if index.name == name: # type: ignore[attr-defined]
764
838
  return index
765
839
  raise ValueError(f"No index named {name} on model {self.name}")
766
840
 
767
- def get_constraint_by_name(self, name):
841
+ def get_constraint_by_name(self, name: str) -> Any:
768
842
  for constraint in self.options["constraints"]:
769
- if constraint.name == name:
843
+ if constraint.name == name: # type: ignore[attr-defined]
770
844
  return constraint
771
845
  raise ValueError(f"No constraint named {name} on model {self.name}")
772
846
 
773
- def __repr__(self):
847
+ def __repr__(self) -> str:
774
848
  return f"<{self.__class__.__name__}: '{self.package_label}.{self.name}'>"
775
849
 
776
- def __eq__(self, other):
850
+ def __eq__(self, other: object) -> bool:
851
+ if not isinstance(other, ModelState):
852
+ return NotImplemented
777
853
  return (
778
854
  (self.package_label == other.package_label)
779
855
  and (self.name == other.name)
780
856
  and (len(self.fields) == len(other.fields))
781
857
  and all(
782
- k1 == k2 and f1.deconstruct()[1:] == f2.deconstruct()[1:]
858
+ k1 == k2 and f1.deconstruct()[1:] == f2.deconstruct()[1:] # type: ignore[attr-defined]
783
859
  for (k1, f1), (k2, f2) in zip(
784
860
  sorted(self.fields.items()),
785
861
  sorted(other.fields.items()),