plain.models 0.49.2__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.
- plain/models/CHANGELOG.md +13 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +22 -12
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +29 -16
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +267 -165
- plain/models/backends/base/validation.py +12 -3
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +12 -3
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +106 -39
- plain/models/backends/mysql/schema.py +48 -24
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +109 -42
- plain/models/backends/postgresql/schema.py +85 -46
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +125 -42
- plain/models/backends/sqlite3/schema.py +82 -58
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +113 -74
- plain/models/cli.py +94 -63
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +65 -47
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +66 -43
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +440 -257
- plain/models/fields/__init__.py +253 -202
- plain/models/fields/json.py +120 -54
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +284 -252
- plain/models/fields/related_descriptors.py +31 -22
- plain/models/fields/related_lookups.py +23 -11
- plain/models/fields/related_managers.py +81 -47
- plain/models/fields/reverse_related.py +58 -55
- plain/models/forms.py +89 -63
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +52 -28
- plain/models/lookups.py +228 -153
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +37 -19
- plain/models/migrations/operations/fields.py +89 -42
- plain/models/migrations/operations/models.py +245 -143
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +18 -11
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +220 -133
- plain/models/migrations/utils.py +29 -13
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +63 -56
- plain/models/otel.py +16 -6
- plain/models/preflight.py +35 -12
- plain/models/query.py +323 -228
- plain/models/query_utils.py +93 -58
- plain/models/registry.py +34 -16
- plain/models/sql/compiler.py +146 -97
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +255 -169
- plain/models/sql/subqueries.py +32 -21
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +13 -5
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
- plain_models-0.50.0.dist-info/RECORD +122 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
plain/models/migrations/state.py
CHANGED
@@ -1,7 +1,10 @@
|
|
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
|
@@ -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(
|
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._meta.package_label, model._meta.model_name # type: ignore[attr-defined]
|
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._meta.get_fields(include_hidden=True): # type: ignore[attr-defined]
|
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._meta.package_label, rel_mod._meta.model_name) # type: ignore[attr-defined]
|
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._meta.package_label, # type: ignore[attr-defined]
|
82
|
+
rel_mod._meta.model_name, # type: ignore[attr-defined]
|
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._meta.package_label, model._meta.model_name)} # type: ignore[attr-defined]
|
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__(
|
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
|
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(
|
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(
|
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(
|
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(
|
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] = [
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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)
|
@@ -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,
|
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(
|
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(
|
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__(
|
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._meta.model_name] = model # type: ignore[attr-defined]
|
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__(
|
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,54 +689,56 @@ 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, "_meta"): # type: ignore[attr-defined]
|
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(field.remote_field.through, "_meta"): # type: ignore[attr-defined]
|
616
698
|
raise ValueError(
|
617
699
|
f'ModelState.fields cannot refer to a model class - "{name}.through" '
|
618
700
|
"does. Use a string reference instead."
|
619
701
|
)
|
620
702
|
# Sanity-check that indexes have their name set.
|
621
703
|
for index in self.options["indexes"]:
|
622
|
-
if not index.name:
|
704
|
+
if not index.name: # type: ignore[attr-defined]
|
623
705
|
raise ValueError(
|
624
706
|
"Indexes passed to ModelState require a name attribute. "
|
625
707
|
f"{index!r} doesn't have one."
|
626
708
|
)
|
627
709
|
|
628
710
|
@cached_property
|
629
|
-
def name_lower(self):
|
711
|
+
def name_lower(self) -> str:
|
630
712
|
return self.name.lower()
|
631
713
|
|
632
|
-
def get_field(self, field_name):
|
714
|
+
def get_field(self, field_name: str) -> Field:
|
633
715
|
return self.fields[field_name]
|
634
716
|
|
635
717
|
@classmethod
|
636
|
-
def from_model(
|
718
|
+
def from_model(
|
719
|
+
cls, model: type[models.Model], exclude_rels: bool = False
|
720
|
+
) -> ModelState:
|
637
721
|
"""Given a model, return a ModelState representing it."""
|
638
722
|
# Deconstruct the fields
|
639
723
|
fields = []
|
640
|
-
for field in model._meta.local_fields:
|
724
|
+
for field in model._meta.local_fields: # type: ignore[attr-defined]
|
641
725
|
if getattr(field, "remote_field", None) and exclude_rels:
|
642
726
|
continue
|
643
|
-
name = field.name
|
727
|
+
name = field.name # type: ignore[attr-defined]
|
644
728
|
try:
|
645
|
-
fields.append((name, field.clone()))
|
729
|
+
fields.append((name, field.clone())) # type: ignore[attr-defined]
|
646
730
|
except TypeError as e:
|
647
731
|
raise TypeError(
|
648
|
-
f"Couldn't reconstruct field {name} on {model._meta.label}: {e}"
|
732
|
+
f"Couldn't reconstruct field {name} on {model._meta.label}: {e}" # type: ignore[attr-defined]
|
649
733
|
)
|
650
734
|
if not exclude_rels:
|
651
|
-
for field in model._meta.local_many_to_many:
|
652
|
-
name = field.name
|
735
|
+
for field in model._meta.local_many_to_many: # type: ignore[attr-defined]
|
736
|
+
name = field.name # type: ignore[attr-defined]
|
653
737
|
try:
|
654
|
-
fields.append((name, field.clone()))
|
738
|
+
fields.append((name, field.clone())) # type: ignore[attr-defined]
|
655
739
|
except TypeError as e:
|
656
740
|
raise TypeError(
|
657
|
-
f"Couldn't reconstruct m2m field {name} on {model._meta.object_name}: {e}"
|
741
|
+
f"Couldn't reconstruct m2m field {name} on {model._meta.object_name}: {e}" # type: ignore[attr-defined]
|
658
742
|
)
|
659
743
|
# Extract the options
|
660
744
|
options = {}
|
@@ -662,24 +746,25 @@ class ModelState:
|
|
662
746
|
# Ignore some special options
|
663
747
|
if name in ["models_registry", "package_label"]:
|
664
748
|
continue
|
665
|
-
elif name in model._meta.original_attrs:
|
749
|
+
elif name in model._meta.original_attrs: # type: ignore[attr-defined]
|
666
750
|
if name == "indexes":
|
667
|
-
indexes = [idx.clone() for idx in model._meta.indexes]
|
751
|
+
indexes = [idx.clone() for idx in model._meta.indexes] # type: ignore[attr-defined]
|
668
752
|
for index in indexes:
|
669
|
-
if not index.name:
|
670
|
-
index.set_name_with_model(model)
|
753
|
+
if not index.name: # type: ignore[attr-defined]
|
754
|
+
index.set_name_with_model(model) # type: ignore[attr-defined]
|
671
755
|
options["indexes"] = indexes
|
672
756
|
elif name == "constraints":
|
673
757
|
options["constraints"] = [
|
674
|
-
con.clone()
|
758
|
+
con.clone()
|
759
|
+
for con in model._meta.constraints # type: ignore[attr-defined]
|
675
760
|
]
|
676
761
|
else:
|
677
|
-
options[name] = model._meta.original_attrs[name]
|
762
|
+
options[name] = model._meta.original_attrs[name] # type: ignore[attr-defined]
|
678
763
|
|
679
|
-
def flatten_bases(model):
|
764
|
+
def flatten_bases(model: type[models.Model]) -> list[type[models.Model]]:
|
680
765
|
bases = []
|
681
766
|
for base in model.__bases__:
|
682
|
-
bases.append(base)
|
767
|
+
bases.append(base) # type: ignore[arg-type]
|
683
768
|
return bases
|
684
769
|
|
685
770
|
# We can't rely on __mro__ directly because we only want to flatten
|
@@ -693,7 +778,7 @@ class ModelState:
|
|
693
778
|
|
694
779
|
# Make our record
|
695
780
|
bases = tuple(
|
696
|
-
(base._meta.label_lower if hasattr(base, "_meta") else base)
|
781
|
+
(base._meta.label_lower if hasattr(base, "_meta") else base) # type: ignore[attr-defined]
|
697
782
|
for base in flattened_bases
|
698
783
|
)
|
699
784
|
# Ensure at least one base inherits from models.Model
|
@@ -704,14 +789,14 @@ class ModelState:
|
|
704
789
|
|
705
790
|
# Construct the new ModelState
|
706
791
|
return cls(
|
707
|
-
model._meta.package_label,
|
708
|
-
model._meta.object_name,
|
792
|
+
model._meta.package_label, # type: ignore[attr-defined]
|
793
|
+
model._meta.object_name, # type: ignore[attr-defined]
|
709
794
|
fields,
|
710
795
|
options,
|
711
796
|
bases,
|
712
797
|
)
|
713
798
|
|
714
|
-
def clone(self):
|
799
|
+
def clone(self) -> ModelState:
|
715
800
|
"""Return an exact copy of this ModelState."""
|
716
801
|
return self.__class__(
|
717
802
|
package_label=self.package_label,
|
@@ -724,7 +809,7 @@ class ModelState:
|
|
724
809
|
bases=self.bases,
|
725
810
|
)
|
726
811
|
|
727
|
-
def render(self, models_registry):
|
812
|
+
def render(self, models_registry: ModelsRegistry) -> type[models.Model]:
|
728
813
|
"""Create a Model object from our current state into the given packages."""
|
729
814
|
# First, make a Meta object
|
730
815
|
meta_contents = {
|
@@ -744,7 +829,7 @@ class ModelState:
|
|
744
829
|
f"Cannot resolve one or more bases from {self.bases!r}"
|
745
830
|
)
|
746
831
|
# Clone fields for the body, add other bits.
|
747
|
-
body = {name: field.clone() for name, field in self.fields.items()}
|
832
|
+
body = {name: field.clone() for name, field in self.fields.items()} # type: ignore[attr-defined]
|
748
833
|
body["Meta"] = meta
|
749
834
|
body["__module__"] = "__fake__"
|
750
835
|
|
@@ -754,32 +839,34 @@ class ModelState:
|
|
754
839
|
|
755
840
|
# Register it to the models_registry associated with the model meta
|
756
841
|
# (could probably do this directly right here too...)
|
757
|
-
register_model(model_class)
|
842
|
+
register_model(model_class) # type: ignore[arg-type]
|
758
843
|
|
759
|
-
return model_class
|
844
|
+
return model_class # type: ignore[return-value]
|
760
845
|
|
761
|
-
def get_index_by_name(self, name):
|
846
|
+
def get_index_by_name(self, name: str) -> Any:
|
762
847
|
for index in self.options["indexes"]:
|
763
|
-
if index.name == name:
|
848
|
+
if index.name == name: # type: ignore[attr-defined]
|
764
849
|
return index
|
765
850
|
raise ValueError(f"No index named {name} on model {self.name}")
|
766
851
|
|
767
|
-
def get_constraint_by_name(self, name):
|
852
|
+
def get_constraint_by_name(self, name: str) -> Any:
|
768
853
|
for constraint in self.options["constraints"]:
|
769
|
-
if constraint.name == name:
|
854
|
+
if constraint.name == name: # type: ignore[attr-defined]
|
770
855
|
return constraint
|
771
856
|
raise ValueError(f"No constraint named {name} on model {self.name}")
|
772
857
|
|
773
|
-
def __repr__(self):
|
858
|
+
def __repr__(self) -> str:
|
774
859
|
return f"<{self.__class__.__name__}: '{self.package_label}.{self.name}'>"
|
775
860
|
|
776
|
-
def __eq__(self, other):
|
861
|
+
def __eq__(self, other: object) -> bool:
|
862
|
+
if not isinstance(other, ModelState):
|
863
|
+
return NotImplemented
|
777
864
|
return (
|
778
865
|
(self.package_label == other.package_label)
|
779
866
|
and (self.name == other.name)
|
780
867
|
and (len(self.fields) == len(other.fields))
|
781
868
|
and all(
|
782
|
-
k1 == k2 and f1.deconstruct()[1:] == f2.deconstruct()[1:]
|
869
|
+
k1 == k2 and f1.deconstruct()[1:] == f2.deconstruct()[1:] # type: ignore[attr-defined]
|
783
870
|
for (k1, f1), (k2, f2) in zip(
|
784
871
|
sorted(self.fields.items()),
|
785
872
|
sorted(other.fields.items()),
|