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.
- plain/models/CHANGELOG.md +27 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -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 +24 -14
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +37 -20
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +338 -218
- plain/models/backends/base/validation.py +13 -4
- 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 +13 -4
- 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 +107 -40
- plain/models/backends/mysql/schema.py +52 -28
- 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 +110 -43
- plain/models/backends/postgresql/schema.py +88 -49
- 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 +126 -43
- plain/models/backends/sqlite3/schema.py +127 -92
- 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 +221 -229
- plain/models/cli.py +98 -67
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +79 -56
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +80 -56
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +441 -258
- plain/models/fields/__init__.py +272 -217
- plain/models/fields/json.py +123 -57
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +324 -290
- plain/models/fields/related_descriptors.py +33 -24
- plain/models/fields/related_lookups.py +24 -12
- plain/models/fields/related_managers.py +102 -79
- plain/models/fields/reverse_related.py +66 -63
- plain/models/forms.py +101 -75
- 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 +57 -32
- plain/models/lookups.py +228 -153
- plain/models/meta.py +505 -0
- 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 +38 -20
- plain/models/migrations/operations/fields.py +95 -48
- plain/models/migrations/operations/models.py +246 -142
- 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 +27 -16
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +232 -156
- plain/models/migrations/utils.py +30 -14
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +189 -518
- plain/models/otel.py +16 -6
- plain/models/preflight.py +42 -17
- plain/models/query.py +400 -251
- plain/models/query_utils.py +109 -69
- plain/models/registry.py +40 -21
- plain/models/sql/compiler.py +190 -127
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +320 -225
- plain/models/sql/subqueries.py +36 -25
- 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 +17 -6
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- plain_models-0.51.0.dist-info/RECORD +123 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/migrations/state.py
CHANGED
@@ -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(
|
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.
|
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.
|
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.
|
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.
|
72
|
-
rel_mod.
|
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.
|
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__(
|
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)
|
@@ -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
|
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,
|
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.
|
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__(
|
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, "
|
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(
|
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(
|
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.
|
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.
|
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.
|
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.
|
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
|
-
(
|
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.
|
708
|
-
model.
|
780
|
+
model.model_options.package_label,
|
781
|
+
model.model_options.object_name,
|
709
782
|
fields,
|
710
|
-
|
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
|
-
#
|
730
|
-
|
731
|
-
|
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["
|
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()),
|