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/fields/related.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from functools import cached_property, partial
|
4
|
+
from typing import TYPE_CHECKING, Any
|
2
5
|
|
3
6
|
from plain import exceptions
|
4
7
|
from plain.models.constants import LOOKUP_SEP
|
@@ -30,10 +33,16 @@ from .related_lookups import (
|
|
30
33
|
)
|
31
34
|
from .reverse_related import ManyToManyRel, ManyToOneRel
|
32
35
|
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
38
|
+
from plain.models.base import Model
|
39
|
+
|
33
40
|
RECURSIVE_RELATIONSHIP_CONSTANT = "self"
|
34
41
|
|
35
42
|
|
36
|
-
def resolve_relation(
|
43
|
+
def resolve_relation(
|
44
|
+
scope_model: type[Model], relation: type[Model] | str
|
45
|
+
) -> type[Model] | str:
|
37
46
|
"""
|
38
47
|
Transform relation into a model or fully-qualified model string of the form
|
39
48
|
"package_label.ModelName", relative to scope_model.
|
@@ -53,12 +62,14 @@ def resolve_relation(scope_model, relation):
|
|
53
62
|
# Look for an "app.Model" relation
|
54
63
|
if isinstance(relation, str):
|
55
64
|
if "." not in relation:
|
56
|
-
relation = f"{scope_model.
|
65
|
+
relation = f"{scope_model.model_options.package_label}.{relation}"
|
57
66
|
|
58
67
|
return relation
|
59
68
|
|
60
69
|
|
61
|
-
def lazy_related_operation(
|
70
|
+
def lazy_related_operation(
|
71
|
+
function: Any, model: type[Model], *related_models: type[Model] | str, **kwargs: Any
|
72
|
+
) -> None:
|
62
73
|
"""
|
63
74
|
Schedule `function` to be called once `model` and all `related_models`
|
64
75
|
have been imported and registered with the app registry. `function` will
|
@@ -71,11 +82,11 @@ def lazy_related_operation(function, model, *related_models, **kwargs):
|
|
71
82
|
references will be resolved relative to `model`.
|
72
83
|
|
73
84
|
This is a convenience wrapper for `Packages.lazy_model_operation` - the app
|
74
|
-
registry model used is the one found in `model.
|
85
|
+
registry model used is the one found in `model._model_meta.models_registry`.
|
75
86
|
"""
|
76
87
|
models = [model] + [resolve_relation(model, rel) for rel in related_models]
|
77
88
|
model_keys = (make_model_tuple(m) for m in models)
|
78
|
-
models_registry = model.
|
89
|
+
models_registry = model._model_meta.models_registry
|
79
90
|
return models_registry.lazy_model_operation(
|
80
91
|
partial(function, **kwargs), *model_keys
|
81
92
|
)
|
@@ -92,35 +103,35 @@ class RelatedField(FieldCacheMixin, Field):
|
|
92
103
|
def __init__(
|
93
104
|
self,
|
94
105
|
*,
|
95
|
-
related_name=None,
|
96
|
-
related_query_name=None,
|
97
|
-
limit_choices_to=None,
|
98
|
-
**kwargs,
|
106
|
+
related_name: str | None = None,
|
107
|
+
related_query_name: str | None = None,
|
108
|
+
limit_choices_to: Any = None,
|
109
|
+
**kwargs: Any,
|
99
110
|
):
|
100
111
|
self._related_name = related_name
|
101
112
|
self._related_query_name = related_query_name
|
102
113
|
self._limit_choices_to = limit_choices_to
|
103
|
-
super().__init__(**kwargs)
|
114
|
+
super().__init__(**kwargs) # type: ignore[misc]
|
104
115
|
|
105
116
|
@cached_property
|
106
|
-
def related_model(self):
|
117
|
+
def related_model(self) -> type[Model]:
|
107
118
|
# Can't cache this property until all the models are loaded.
|
108
119
|
models_registry.check_ready()
|
109
|
-
return self.remote_field.model
|
120
|
+
return self.remote_field.model # type: ignore[attr-defined]
|
110
121
|
|
111
|
-
def preflight(self, **kwargs):
|
122
|
+
def preflight(self, **kwargs: Any) -> list[PreflightResult]: # type: ignore[misc]
|
112
123
|
return [
|
113
|
-
*super().preflight(**kwargs),
|
124
|
+
*super().preflight(**kwargs), # type: ignore[misc]
|
114
125
|
*self._check_related_name_is_valid(),
|
115
126
|
*self._check_related_query_name_is_valid(),
|
116
127
|
*self._check_relation_model_exists(),
|
117
128
|
*self._check_clashes(),
|
118
129
|
]
|
119
130
|
|
120
|
-
def _check_related_name_is_valid(self):
|
131
|
+
def _check_related_name_is_valid(self) -> list[PreflightResult]:
|
121
132
|
import keyword
|
122
133
|
|
123
|
-
related_name = self.remote_field.related_name
|
134
|
+
related_name = self.remote_field.related_name # type: ignore[attr-defined]
|
124
135
|
if related_name is None:
|
125
136
|
return []
|
126
137
|
is_valid_id = (
|
@@ -129,18 +140,18 @@ class RelatedField(FieldCacheMixin, Field):
|
|
129
140
|
if not is_valid_id:
|
130
141
|
return [
|
131
142
|
PreflightResult(
|
132
|
-
fix=f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model.
|
143
|
+
fix=f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model.model_options.object_name}.{self.name}. Related name must be a valid Python identifier.",
|
133
144
|
obj=self,
|
134
145
|
id="fields.invalid_related_name",
|
135
146
|
)
|
136
147
|
]
|
137
148
|
return []
|
138
149
|
|
139
|
-
def _check_related_query_name_is_valid(self):
|
140
|
-
if self.remote_field.is_hidden():
|
150
|
+
def _check_related_query_name_is_valid(self) -> list[PreflightResult]:
|
151
|
+
if self.remote_field.is_hidden(): # type: ignore[attr-defined]
|
141
152
|
return []
|
142
153
|
rel_query_name = self.related_query_name()
|
143
|
-
errors = []
|
154
|
+
errors: list[PreflightResult] = []
|
144
155
|
if rel_query_name.endswith("_"):
|
145
156
|
errors.append(
|
146
157
|
PreflightResult(
|
@@ -167,15 +178,15 @@ class RelatedField(FieldCacheMixin, Field):
|
|
167
178
|
)
|
168
179
|
return errors
|
169
180
|
|
170
|
-
def _check_relation_model_exists(self):
|
181
|
+
def _check_relation_model_exists(self) -> list[PreflightResult]:
|
171
182
|
rel_is_missing = (
|
172
|
-
self.remote_field.model not in self.
|
183
|
+
self.remote_field.model not in self.meta.models_registry.get_models() # type: ignore[attr-defined]
|
173
184
|
)
|
174
|
-
rel_is_string = isinstance(self.remote_field.model, str)
|
185
|
+
rel_is_string = isinstance(self.remote_field.model, str) # type: ignore[attr-defined]
|
175
186
|
model_name = (
|
176
|
-
self.remote_field.model
|
187
|
+
self.remote_field.model # type: ignore[attr-defined]
|
177
188
|
if rel_is_string
|
178
|
-
else self.remote_field.model.
|
189
|
+
else self.remote_field.model.model_options.object_name
|
179
190
|
)
|
180
191
|
if rel_is_missing and rel_is_string:
|
181
192
|
return [
|
@@ -190,16 +201,15 @@ class RelatedField(FieldCacheMixin, Field):
|
|
190
201
|
]
|
191
202
|
return []
|
192
203
|
|
193
|
-
def _check_clashes(self):
|
204
|
+
def _check_clashes(self) -> list[PreflightResult]:
|
194
205
|
"""Check accessor and reverse query name clashes."""
|
195
206
|
from plain.models.base import ModelBase
|
196
207
|
|
197
|
-
errors = []
|
198
|
-
opts = self.model._meta
|
208
|
+
errors: list[PreflightResult] = []
|
199
209
|
|
200
210
|
# f.remote_field.model may be a string instead of a model. Skip if
|
201
211
|
# model name is not resolved.
|
202
|
-
if not isinstance(self.remote_field.model, ModelBase):
|
212
|
+
if not isinstance(self.remote_field.model, ModelBase): # type: ignore[attr-defined]
|
203
213
|
return []
|
204
214
|
|
205
215
|
# Consider that we are checking field `Model.foreign` and the models
|
@@ -213,29 +223,32 @@ class RelatedField(FieldCacheMixin, Field):
|
|
213
223
|
# foreign = models.ForeignKey(Target)
|
214
224
|
# m2m = models.ManyToManyField(Target)
|
215
225
|
|
216
|
-
#
|
217
|
-
|
226
|
+
# rel_options.object_name == "Target"
|
227
|
+
rel_meta = self.remote_field.model._model_meta
|
228
|
+
rel_options = self.remote_field.model.model_options
|
218
229
|
# If the field doesn't install a backward relation on the target model
|
219
230
|
# (so `is_hidden` returns True), then there are no clashes to check
|
220
231
|
# and we can skip these fields.
|
221
|
-
rel_is_hidden = self.remote_field.is_hidden()
|
222
|
-
rel_name =
|
232
|
+
rel_is_hidden = self.remote_field.is_hidden() # type: ignore[attr-defined]
|
233
|
+
rel_name = (
|
234
|
+
self.remote_field.get_accessor_name()
|
235
|
+
) # i. e. "model_set" # type: ignore[attr-defined]
|
223
236
|
rel_query_name = self.related_query_name() # i. e. "model"
|
224
237
|
# i.e. "package_label.Model.field".
|
225
|
-
field_name = f"{
|
238
|
+
field_name = f"{self.model.model_options.label}.{self.name}"
|
226
239
|
|
227
240
|
# Check clashes between accessor or reverse query name of `field`
|
228
241
|
# and any other field name -- i.e. accessor for Model.foreign is
|
229
242
|
# model_set and it clashes with Target.model_set.
|
230
|
-
potential_clashes =
|
243
|
+
potential_clashes = rel_meta.fields + rel_meta.many_to_many
|
231
244
|
for clash_field in potential_clashes:
|
232
245
|
# i.e. "package_label.Target.model_set".
|
233
|
-
clash_name = f"{
|
246
|
+
clash_name = f"{rel_options.label}.{clash_field.name}"
|
234
247
|
if not rel_is_hidden and clash_field.name == rel_name:
|
235
248
|
errors.append(
|
236
249
|
PreflightResult(
|
237
250
|
fix=(
|
238
|
-
f"Reverse accessor '{
|
251
|
+
f"Reverse accessor '{rel_options.object_name}.{rel_name}' "
|
239
252
|
f"for '{field_name}' clashes with field name "
|
240
253
|
f"'{clash_name}'. "
|
241
254
|
f"Rename field '{clash_name}', or add/change a related_name "
|
@@ -262,17 +275,15 @@ class RelatedField(FieldCacheMixin, Field):
|
|
262
275
|
# Check clashes between accessors/reverse query names of `field` and
|
263
276
|
# any other field accessor -- i. e. Model.foreign accessor clashes with
|
264
277
|
# Model.m2m accessor.
|
265
|
-
potential_clashes = (r for r in
|
278
|
+
potential_clashes = (r for r in rel_meta.related_objects if r.field is not self)
|
266
279
|
for clash_field in potential_clashes:
|
267
280
|
# i.e. "package_label.Model.m2m".
|
268
|
-
clash_name =
|
269
|
-
f"{clash_field.related_model._meta.label}.{clash_field.field.name}"
|
270
|
-
)
|
281
|
+
clash_name = f"{clash_field.related_model.model_options.label}.{clash_field.field.name}"
|
271
282
|
if not rel_is_hidden and clash_field.get_accessor_name() == rel_name:
|
272
283
|
errors.append(
|
273
284
|
PreflightResult(
|
274
285
|
fix=(
|
275
|
-
f"Reverse accessor '{
|
286
|
+
f"Reverse accessor '{rel_options.object_name}.{rel_name}' "
|
276
287
|
f"for '{field_name}' clashes with reverse accessor for "
|
277
288
|
f"'{clash_name}'. "
|
278
289
|
"Add or change a related_name argument "
|
@@ -299,42 +310,47 @@ class RelatedField(FieldCacheMixin, Field):
|
|
299
310
|
|
300
311
|
return errors
|
301
312
|
|
302
|
-
def db_type(self, connection):
|
313
|
+
def db_type(self, connection: BaseDatabaseWrapper) -> None:
|
303
314
|
# By default related field will not have a column as it relates to
|
304
315
|
# columns from another table.
|
305
316
|
return None
|
306
317
|
|
307
|
-
def contribute_to_class(self, cls, name):
|
308
|
-
super().contribute_to_class(cls, name)
|
318
|
+
def contribute_to_class(self, cls: type[Model], name: str) -> None:
|
319
|
+
super().contribute_to_class(cls, name) # type: ignore[misc]
|
309
320
|
|
310
|
-
self.
|
321
|
+
self.meta = cls._model_meta
|
311
322
|
|
312
|
-
if self.remote_field.related_name:
|
313
|
-
related_name = self.remote_field.related_name
|
323
|
+
if self.remote_field.related_name: # type: ignore[attr-defined]
|
324
|
+
related_name = self.remote_field.related_name # type: ignore[attr-defined]
|
314
325
|
related_name %= {
|
315
326
|
"class": cls.__name__.lower(),
|
316
|
-
"model_name": cls.
|
317
|
-
"package_label": cls.
|
327
|
+
"model_name": cls.model_options.model_name.lower(),
|
328
|
+
"package_label": cls.model_options.package_label.lower(),
|
318
329
|
}
|
319
|
-
self.remote_field.related_name = related_name
|
330
|
+
self.remote_field.related_name = related_name # type: ignore[attr-defined]
|
320
331
|
|
321
|
-
if self.remote_field.related_query_name:
|
322
|
-
related_query_name = self.remote_field.related_query_name % {
|
332
|
+
if self.remote_field.related_query_name: # type: ignore[attr-defined]
|
333
|
+
related_query_name = self.remote_field.related_query_name % { # type: ignore[attr-defined]
|
323
334
|
"class": cls.__name__.lower(),
|
324
|
-
"package_label": cls.
|
335
|
+
"package_label": cls.model_options.package_label.lower(),
|
325
336
|
}
|
326
|
-
self.remote_field.related_query_name = related_query_name
|
337
|
+
self.remote_field.related_query_name = related_query_name # type: ignore[attr-defined]
|
327
338
|
|
328
|
-
def resolve_related_class(
|
329
|
-
|
339
|
+
def resolve_related_class(
|
340
|
+
model: type[Model], related: type[Model], field: RelatedField
|
341
|
+
) -> None:
|
342
|
+
field.remote_field.model = related # type: ignore[attr-defined]
|
330
343
|
field.do_related_class(related, model)
|
331
344
|
|
332
345
|
lazy_related_operation(
|
333
|
-
resolve_related_class,
|
346
|
+
resolve_related_class,
|
347
|
+
cls,
|
348
|
+
self.remote_field.model,
|
349
|
+
field=self, # type: ignore[attr-defined]
|
334
350
|
)
|
335
351
|
|
336
|
-
def deconstruct(self):
|
337
|
-
name, path, args, kwargs = super().deconstruct()
|
352
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
353
|
+
name, path, args, kwargs = super().deconstruct() # type: ignore[misc]
|
338
354
|
if self._limit_choices_to:
|
339
355
|
kwargs["limit_choices_to"] = self._limit_choices_to
|
340
356
|
if self._related_name is not None:
|
@@ -343,7 +359,7 @@ class RelatedField(FieldCacheMixin, Field):
|
|
343
359
|
kwargs["related_query_name"] = self._related_query_name
|
344
360
|
return name, path, args, kwargs
|
345
361
|
|
346
|
-
def get_forward_related_filter(self, obj):
|
362
|
+
def get_forward_related_filter(self, obj: Model) -> dict[str, Any]:
|
347
363
|
"""
|
348
364
|
Return the keyword arguments that when supplied to
|
349
365
|
self.model.object.filter(), would select all instances related through
|
@@ -353,10 +369,10 @@ class RelatedField(FieldCacheMixin, Field):
|
|
353
369
|
"""
|
354
370
|
return {
|
355
371
|
f"{self.name}__{rh_field.name}": getattr(obj, rh_field.attname)
|
356
|
-
for _, rh_field in self.related_fields
|
372
|
+
for _, rh_field in self.related_fields # type: ignore[attr-defined]
|
357
373
|
}
|
358
374
|
|
359
|
-
def get_reverse_related_filter(self, obj):
|
375
|
+
def get_reverse_related_filter(self, obj: Model) -> Any:
|
360
376
|
"""
|
361
377
|
Complement to get_forward_related_filter(). Return the keyword
|
362
378
|
arguments that when passed to self.related_field.model.object.filter()
|
@@ -366,47 +382,49 @@ class RelatedField(FieldCacheMixin, Field):
|
|
366
382
|
return Q.create(
|
367
383
|
[
|
368
384
|
(rh_field.attname, getattr(obj, lh_field.attname))
|
369
|
-
for lh_field, rh_field in self.related_fields
|
385
|
+
for lh_field, rh_field in self.related_fields # type: ignore[attr-defined]
|
370
386
|
]
|
371
387
|
)
|
372
388
|
|
373
|
-
def set_attributes_from_rel(self):
|
374
|
-
self.name = self.name or (
|
375
|
-
|
389
|
+
def set_attributes_from_rel(self) -> None:
|
390
|
+
self.name = self.name or (
|
391
|
+
self.remote_field.model.model_options.model_name + "_" + "id"
|
392
|
+
)
|
393
|
+
self.remote_field.set_field_name() # type: ignore[attr-defined]
|
376
394
|
|
377
|
-
def do_related_class(self, other, cls):
|
395
|
+
def do_related_class(self, other: type[Model], cls: type[Model]) -> None:
|
378
396
|
self.set_attributes_from_rel()
|
379
|
-
self.contribute_to_related_class(other, self.remote_field)
|
397
|
+
self.contribute_to_related_class(other, self.remote_field) # type: ignore[attr-defined]
|
380
398
|
|
381
|
-
def get_limit_choices_to(self):
|
399
|
+
def get_limit_choices_to(self) -> Any:
|
382
400
|
"""
|
383
401
|
Return ``limit_choices_to`` for this model field.
|
384
402
|
|
385
403
|
If it is a callable, it will be invoked and the result will be
|
386
404
|
returned.
|
387
405
|
"""
|
388
|
-
if callable(self.remote_field.limit_choices_to):
|
389
|
-
return self.remote_field.limit_choices_to()
|
390
|
-
return self.remote_field.limit_choices_to
|
406
|
+
if callable(self.remote_field.limit_choices_to): # type: ignore[attr-defined]
|
407
|
+
return self.remote_field.limit_choices_to() # type: ignore[attr-defined]
|
408
|
+
return self.remote_field.limit_choices_to # type: ignore[attr-defined]
|
391
409
|
|
392
|
-
def related_query_name(self):
|
410
|
+
def related_query_name(self) -> str:
|
393
411
|
"""
|
394
412
|
Define the name that can be used to identify this related object in a
|
395
413
|
table-spanning query.
|
396
414
|
"""
|
397
415
|
return (
|
398
|
-
self.remote_field.related_query_name
|
399
|
-
or self.remote_field.related_name
|
400
|
-
or self.
|
416
|
+
self.remote_field.related_query_name # type: ignore[attr-defined]
|
417
|
+
or self.remote_field.related_name # type: ignore[attr-defined]
|
418
|
+
or self.model.model_options.model_name # type: ignore[attr-defined]
|
401
419
|
)
|
402
420
|
|
403
421
|
@property
|
404
|
-
def target_field(self):
|
422
|
+
def target_field(self) -> Field:
|
405
423
|
"""
|
406
424
|
When filtering against this relation, return the field on the remote
|
407
425
|
model against which the filtering should happen.
|
408
426
|
"""
|
409
|
-
target_fields = self.path_infos[-1].target_fields
|
427
|
+
target_fields = self.path_infos[-1].target_fields # type: ignore[attr-defined]
|
410
428
|
if len(target_fields) > 1:
|
411
429
|
raise FieldError(
|
412
430
|
"The relation has multiple target fields, but only single target field "
|
@@ -414,8 +432,8 @@ class RelatedField(FieldCacheMixin, Field):
|
|
414
432
|
)
|
415
433
|
return target_fields[0]
|
416
434
|
|
417
|
-
def get_cache_name(self):
|
418
|
-
return self.name
|
435
|
+
def get_cache_name(self) -> str:
|
436
|
+
return self.name # type: ignore[return-value]
|
419
437
|
|
420
438
|
|
421
439
|
class ForeignKey(RelatedField):
|
@@ -442,19 +460,19 @@ class ForeignKey(RelatedField):
|
|
442
460
|
|
443
461
|
def __init__(
|
444
462
|
self,
|
445
|
-
to,
|
446
|
-
on_delete,
|
447
|
-
related_name=None,
|
448
|
-
related_query_name=None,
|
449
|
-
limit_choices_to=None,
|
450
|
-
db_index=True,
|
451
|
-
db_constraint=True,
|
452
|
-
**kwargs,
|
463
|
+
to: type[Model] | str,
|
464
|
+
on_delete: Any,
|
465
|
+
related_name: str | None = None,
|
466
|
+
related_query_name: str | None = None,
|
467
|
+
limit_choices_to: Any = None,
|
468
|
+
db_index: bool = True,
|
469
|
+
db_constraint: bool = True,
|
470
|
+
**kwargs: Any,
|
453
471
|
):
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
472
|
+
if not isinstance(to, str):
|
473
|
+
try:
|
474
|
+
to.model_options.model_name
|
475
|
+
except AttributeError:
|
458
476
|
raise TypeError(
|
459
477
|
f"{self.__class__.__name__}({to!r}) is invalid. First parameter to ForeignKey must be "
|
460
478
|
f"either a model, a model name, or the string {RECURSIVE_RELATIONSHIP_CONSTANT!r}"
|
@@ -481,55 +499,57 @@ class ForeignKey(RelatedField):
|
|
481
499
|
self.db_index = db_index
|
482
500
|
self.db_constraint = db_constraint
|
483
501
|
|
484
|
-
def __copy__(self):
|
485
|
-
obj = super().__copy__()
|
502
|
+
def __copy__(self) -> ForeignKey:
|
503
|
+
obj = super().__copy__() # type: ignore[misc]
|
486
504
|
# Remove any cached PathInfo values.
|
487
505
|
obj.__dict__.pop("path_infos", None)
|
488
506
|
obj.__dict__.pop("reverse_path_infos", None)
|
489
507
|
return obj
|
490
508
|
|
491
509
|
@cached_property
|
492
|
-
def related_fields(self):
|
510
|
+
def related_fields(self) -> list[tuple[Field, Field]]:
|
493
511
|
return self.resolve_related_fields()
|
494
512
|
|
495
513
|
@cached_property
|
496
|
-
def reverse_related_fields(self):
|
514
|
+
def reverse_related_fields(self) -> list[tuple[Field, Field]]:
|
497
515
|
return [(rhs_field, lhs_field) for lhs_field, rhs_field in self.related_fields]
|
498
516
|
|
499
517
|
@cached_property
|
500
|
-
def local_related_fields(self):
|
518
|
+
def local_related_fields(self) -> tuple[Field, ...]:
|
501
519
|
return tuple(lhs_field for lhs_field, rhs_field in self.related_fields)
|
502
520
|
|
503
521
|
@cached_property
|
504
|
-
def foreign_related_fields(self):
|
522
|
+
def foreign_related_fields(self) -> tuple[Field, ...]:
|
505
523
|
return tuple(
|
506
524
|
rhs_field for lhs_field, rhs_field in self.related_fields if rhs_field
|
507
525
|
)
|
508
526
|
|
509
|
-
def get_local_related_value(self, instance):
|
527
|
+
def get_local_related_value(self, instance: Model) -> tuple[Any, ...]:
|
510
528
|
# Always returns the value of the single local field
|
511
529
|
field = self.local_related_fields[0]
|
512
|
-
if field.primary_key:
|
530
|
+
if field.primary_key: # type: ignore[attr-defined]
|
513
531
|
return (instance.id,)
|
514
|
-
return (getattr(instance, field.attname),)
|
532
|
+
return (getattr(instance, field.attname),) # type: ignore[attr-defined]
|
515
533
|
|
516
|
-
def get_foreign_related_value(self, instance):
|
534
|
+
def get_foreign_related_value(self, instance: Model) -> tuple[Any, ...]:
|
517
535
|
# Always returns the id of the foreign instance
|
518
536
|
return (instance.id,)
|
519
537
|
|
520
|
-
def get_joining_columns(
|
538
|
+
def get_joining_columns(
|
539
|
+
self, reverse_join: bool = False
|
540
|
+
) -> tuple[tuple[str, str], ...]:
|
521
541
|
# Always returns a single column pair
|
522
542
|
if reverse_join:
|
523
543
|
from_field, to_field = self.related_fields[0]
|
524
|
-
return ((to_field.column, from_field.column),)
|
544
|
+
return ((to_field.column, from_field.column),) # type: ignore[attr-defined]
|
525
545
|
else:
|
526
546
|
from_field, to_field = self.related_fields[0]
|
527
|
-
return ((from_field.column, to_field.column),)
|
547
|
+
return ((from_field.column, to_field.column),) # type: ignore[attr-defined]
|
528
548
|
|
529
|
-
def get_reverse_joining_columns(self):
|
549
|
+
def get_reverse_joining_columns(self) -> tuple[tuple[str, str], ...]:
|
530
550
|
return self.get_joining_columns(reverse_join=True)
|
531
551
|
|
532
|
-
def get_extra_restriction(self, alias, related_alias):
|
552
|
+
def get_extra_restriction(self, alias: str, related_alias: str) -> None:
|
533
553
|
"""
|
534
554
|
Return a pair condition used for joining and subquery pushdown. The
|
535
555
|
condition is something that responds to as_sql(compiler, connection)
|
@@ -543,14 +563,14 @@ class ForeignKey(RelatedField):
|
|
543
563
|
"""
|
544
564
|
return None
|
545
565
|
|
546
|
-
def get_path_info(self, filtered_relation=None):
|
566
|
+
def get_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
|
547
567
|
"""Get path from this field to the related model."""
|
548
|
-
|
549
|
-
|
568
|
+
meta = self.remote_field.model._model_meta
|
569
|
+
from_meta = self.model._model_meta
|
550
570
|
return [
|
551
571
|
PathInfo(
|
552
|
-
|
553
|
-
|
572
|
+
from_meta=from_meta,
|
573
|
+
to_meta=meta,
|
554
574
|
target_fields=self.foreign_related_fields,
|
555
575
|
join_field=self,
|
556
576
|
m2m=False,
|
@@ -560,58 +580,58 @@ class ForeignKey(RelatedField):
|
|
560
580
|
]
|
561
581
|
|
562
582
|
@cached_property
|
563
|
-
def path_infos(self):
|
583
|
+
def path_infos(self) -> list[PathInfo]:
|
564
584
|
return self.get_path_info()
|
565
585
|
|
566
|
-
def get_reverse_path_info(self, filtered_relation=None):
|
586
|
+
def get_reverse_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
|
567
587
|
"""Get path from the related model to this field's model."""
|
568
|
-
|
569
|
-
|
588
|
+
meta = self.model._model_meta
|
589
|
+
from_meta = self.remote_field.model._model_meta
|
570
590
|
return [
|
571
591
|
PathInfo(
|
572
|
-
|
573
|
-
|
574
|
-
target_fields=(
|
575
|
-
join_field=self.remote_field,
|
576
|
-
m2m=not self.primary_key,
|
592
|
+
from_meta=from_meta,
|
593
|
+
to_meta=meta,
|
594
|
+
target_fields=(meta.get_field("id"),),
|
595
|
+
join_field=self.remote_field, # type: ignore[attr-defined]
|
596
|
+
m2m=not self.primary_key, # type: ignore[attr-defined]
|
577
597
|
direct=False,
|
578
598
|
filtered_relation=filtered_relation,
|
579
599
|
)
|
580
600
|
]
|
581
601
|
|
582
602
|
@cached_property
|
583
|
-
def reverse_path_infos(self):
|
603
|
+
def reverse_path_infos(self) -> list[PathInfo]:
|
584
604
|
return self.get_reverse_path_info()
|
585
605
|
|
586
|
-
def contribute_to_class(self, cls, name):
|
606
|
+
def contribute_to_class(self, cls: type[Model], name: str) -> None:
|
587
607
|
super().contribute_to_class(cls, name)
|
588
|
-
setattr(cls, self.name, self.forward_related_accessor_class(self))
|
608
|
+
setattr(cls, self.name, self.forward_related_accessor_class(self)) # type: ignore[attr-defined]
|
589
609
|
|
590
|
-
def contribute_to_related_class(self, cls, related):
|
610
|
+
def contribute_to_related_class(self, cls: type[Model], related: Any) -> None:
|
591
611
|
# Internal FK's - i.e., those with a related name ending with '+'
|
592
|
-
if not self.remote_field.is_hidden():
|
612
|
+
if not self.remote_field.is_hidden(): # type: ignore[attr-defined]
|
593
613
|
setattr(
|
594
614
|
cls,
|
595
615
|
related.get_accessor_name(),
|
596
|
-
self.related_accessor_class(related),
|
616
|
+
self.related_accessor_class(related), # type: ignore[attr-defined]
|
597
617
|
)
|
598
618
|
# While 'limit_choices_to' might be a callable, simply pass
|
599
619
|
# it along for later - this is too early because it's still
|
600
620
|
# model load time.
|
601
|
-
if self.remote_field.limit_choices_to:
|
602
|
-
cls.
|
603
|
-
self.remote_field.limit_choices_to
|
621
|
+
if self.remote_field.limit_choices_to: # type: ignore[attr-defined]
|
622
|
+
cls._model_meta.related_fkey_lookups.append(
|
623
|
+
self.remote_field.limit_choices_to # type: ignore[attr-defined]
|
604
624
|
)
|
605
625
|
|
606
|
-
def preflight(self, **kwargs):
|
626
|
+
def preflight(self, **kwargs: Any) -> list[PreflightResult]: # type: ignore[misc]
|
607
627
|
return [
|
608
|
-
*super().preflight(**kwargs),
|
628
|
+
*super().preflight(**kwargs), # type: ignore[misc]
|
609
629
|
*self._check_on_delete(),
|
610
630
|
]
|
611
631
|
|
612
|
-
def _check_on_delete(self):
|
613
|
-
on_delete = getattr(self.remote_field, "on_delete", None)
|
614
|
-
if on_delete == SET_NULL and not self.allow_null:
|
632
|
+
def _check_on_delete(self) -> list[PreflightResult]:
|
633
|
+
on_delete = getattr(self.remote_field, "on_delete", None) # type: ignore[attr-defined]
|
634
|
+
if on_delete == SET_NULL and not self.allow_null: # type: ignore[attr-defined]
|
615
635
|
return [
|
616
636
|
PreflightResult(
|
617
637
|
fix=(
|
@@ -622,7 +642,7 @@ class ForeignKey(RelatedField):
|
|
622
642
|
id="fields.foreign_key_null_constraint_violation",
|
623
643
|
)
|
624
644
|
]
|
625
|
-
elif on_delete == SET_DEFAULT and not self.has_default():
|
645
|
+
elif on_delete == SET_DEFAULT and not self.has_default(): # type: ignore[attr-defined]
|
626
646
|
return [
|
627
647
|
PreflightResult(
|
628
648
|
fix=(
|
@@ -636,20 +656,20 @@ class ForeignKey(RelatedField):
|
|
636
656
|
else:
|
637
657
|
return []
|
638
658
|
|
639
|
-
def deconstruct(self):
|
659
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
640
660
|
name, path, args, kwargs = super().deconstruct()
|
641
|
-
kwargs["on_delete"] = self.remote_field.on_delete
|
661
|
+
kwargs["on_delete"] = self.remote_field.on_delete # type: ignore[attr-defined]
|
642
662
|
|
643
|
-
if isinstance(self.remote_field.model, SettingsReference):
|
644
|
-
kwargs["to"] = self.remote_field.model
|
645
|
-
elif isinstance(self.remote_field.model, str):
|
646
|
-
if "." in self.remote_field.model:
|
647
|
-
package_label, model_name = self.remote_field.model.split(".")
|
663
|
+
if isinstance(self.remote_field.model, SettingsReference): # type: ignore[attr-defined]
|
664
|
+
kwargs["to"] = self.remote_field.model # type: ignore[attr-defined]
|
665
|
+
elif isinstance(self.remote_field.model, str): # type: ignore[attr-defined]
|
666
|
+
if "." in self.remote_field.model: # type: ignore[attr-defined]
|
667
|
+
package_label, model_name = self.remote_field.model.split(".") # type: ignore[attr-defined]
|
648
668
|
kwargs["to"] = f"{package_label}.{model_name.lower()}"
|
649
669
|
else:
|
650
|
-
kwargs["to"] = self.remote_field.model.lower()
|
670
|
+
kwargs["to"] = self.remote_field.model.lower() # type: ignore[attr-defined]
|
651
671
|
else:
|
652
|
-
kwargs["to"] = self.remote_field.model.
|
672
|
+
kwargs["to"] = self.remote_field.model.model_options.label_lower
|
653
673
|
|
654
674
|
if self.db_index is not True:
|
655
675
|
kwargs["db_index"] = self.db_index
|
@@ -659,105 +679,107 @@ class ForeignKey(RelatedField):
|
|
659
679
|
|
660
680
|
return name, path, args, kwargs
|
661
681
|
|
662
|
-
def to_python(self, value):
|
663
|
-
return self.target_field.to_python(value)
|
682
|
+
def to_python(self, value: Any) -> Any:
|
683
|
+
return self.target_field.to_python(value) # type: ignore[attr-defined]
|
664
684
|
|
665
685
|
@property
|
666
|
-
def target_field(self):
|
686
|
+
def target_field(self) -> Field:
|
667
687
|
return self.foreign_related_fields[0]
|
668
688
|
|
669
|
-
def validate(self, value, model_instance):
|
670
|
-
super().validate(value, model_instance)
|
689
|
+
def validate(self, value: Any, model_instance: Model) -> None:
|
690
|
+
super().validate(value, model_instance) # type: ignore[misc]
|
671
691
|
if value is None:
|
672
|
-
return
|
692
|
+
return None
|
673
693
|
|
674
|
-
qs = self.remote_field.model.
|
675
|
-
**{self.remote_field.field_name: value}
|
694
|
+
qs = self.remote_field.model._model_meta.base_queryset.filter(
|
695
|
+
**{self.remote_field.field_name: value} # type: ignore[attr-defined]
|
676
696
|
)
|
677
697
|
qs = qs.complex_filter(self.get_limit_choices_to())
|
678
698
|
if not qs.exists():
|
679
699
|
raise exceptions.ValidationError(
|
680
|
-
self.error_messages["invalid"],
|
700
|
+
self.error_messages["invalid"], # type: ignore[attr-defined]
|
681
701
|
code="invalid",
|
682
702
|
params={
|
683
|
-
"model": self.remote_field.model.
|
703
|
+
"model": self.remote_field.model.model_options.model_name,
|
684
704
|
"id": value,
|
685
|
-
"field": self.remote_field.field_name,
|
705
|
+
"field": self.remote_field.field_name, # type: ignore[attr-defined]
|
686
706
|
"value": value,
|
687
707
|
},
|
688
708
|
)
|
689
709
|
|
690
|
-
def resolve_related_fields(self):
|
691
|
-
if isinstance(self.remote_field.model, str):
|
710
|
+
def resolve_related_fields(self) -> list[tuple[ForeignKey, Field]]:
|
711
|
+
if isinstance(self.remote_field.model, str): # type: ignore[attr-defined]
|
692
712
|
raise ValueError(
|
693
|
-
f"Related model {self.remote_field.model!r} cannot be resolved"
|
713
|
+
f"Related model {self.remote_field.model!r} cannot be resolved" # type: ignore[attr-defined]
|
694
714
|
)
|
695
715
|
from_field = self
|
696
|
-
to_field = self.remote_field.model.
|
697
|
-
related_fields = [(from_field, to_field)]
|
716
|
+
to_field = self.remote_field.model._model_meta.get_field("id")
|
717
|
+
related_fields: list[tuple[ForeignKey, Field]] = [(from_field, to_field)]
|
698
718
|
|
699
719
|
for from_field, to_field in related_fields:
|
700
|
-
if to_field and to_field.model != self.remote_field.model:
|
720
|
+
if to_field and to_field.model != self.remote_field.model: # type: ignore[attr-defined]
|
701
721
|
raise FieldError(
|
702
|
-
f"'{self.model.
|
703
|
-
f"'{self.remote_field.model.
|
722
|
+
f"'{self.model.model_options.label}.{self.name}' refers to field '{to_field.name}' which is not local to model "
|
723
|
+
f"'{self.remote_field.model.model_options.label}'."
|
704
724
|
)
|
705
725
|
return related_fields
|
706
726
|
|
707
|
-
def get_attname(self):
|
727
|
+
def get_attname(self) -> str:
|
708
728
|
return f"{self.name}_id"
|
709
729
|
|
710
|
-
def get_attname_column(self):
|
730
|
+
def get_attname_column(self) -> tuple[str, str]:
|
711
731
|
attname = self.get_attname()
|
712
|
-
column = self.db_column or attname
|
732
|
+
column = self.db_column or attname # type: ignore[attr-defined]
|
713
733
|
return attname, column
|
714
734
|
|
715
|
-
def get_default(self):
|
735
|
+
def get_default(self) -> Any:
|
716
736
|
"""Return the to_field if the default value is an object."""
|
717
|
-
field_default = super().get_default()
|
718
|
-
if isinstance(field_default, self.remote_field.model):
|
719
|
-
return getattr(field_default, self.target_field.attname)
|
737
|
+
field_default = super().get_default() # type: ignore[misc]
|
738
|
+
if isinstance(field_default, self.remote_field.model): # type: ignore[attr-defined]
|
739
|
+
return getattr(field_default, self.target_field.attname) # type: ignore[attr-defined]
|
720
740
|
return field_default
|
721
741
|
|
722
|
-
def get_db_prep_save(self, value, connection):
|
742
|
+
def get_db_prep_save(self, value: Any, connection: BaseDatabaseWrapper) -> Any:
|
723
743
|
if value is None or (
|
724
|
-
value == "" and not self.target_field.empty_strings_allowed
|
744
|
+
value == "" and not self.target_field.empty_strings_allowed # type: ignore[attr-defined]
|
725
745
|
):
|
726
746
|
return None
|
727
747
|
else:
|
728
|
-
return self.target_field.get_db_prep_save(value, connection=connection)
|
748
|
+
return self.target_field.get_db_prep_save(value, connection=connection) # type: ignore[attr-defined]
|
729
749
|
|
730
|
-
def get_db_prep_value(
|
731
|
-
|
750
|
+
def get_db_prep_value(
|
751
|
+
self, value: Any, connection: BaseDatabaseWrapper, prepared: bool = False
|
752
|
+
) -> Any:
|
753
|
+
return self.target_field.get_db_prep_value(value, connection, prepared) # type: ignore[attr-defined]
|
732
754
|
|
733
|
-
def get_prep_value(self, value):
|
734
|
-
return self.target_field.get_prep_value(value)
|
755
|
+
def get_prep_value(self, value: Any) -> Any:
|
756
|
+
return self.target_field.get_prep_value(value) # type: ignore[attr-defined]
|
735
757
|
|
736
|
-
def db_check(self, connection):
|
758
|
+
def db_check(self, connection: BaseDatabaseWrapper) -> None:
|
737
759
|
return None
|
738
760
|
|
739
|
-
def db_type(self, connection):
|
740
|
-
return self.target_field.rel_db_type(connection=connection)
|
761
|
+
def db_type(self, connection: BaseDatabaseWrapper) -> str | None:
|
762
|
+
return self.target_field.rel_db_type(connection=connection) # type: ignore[attr-defined]
|
741
763
|
|
742
|
-
def cast_db_type(self, connection):
|
743
|
-
return self.target_field.cast_db_type(connection=connection)
|
764
|
+
def cast_db_type(self, connection: BaseDatabaseWrapper) -> str | None:
|
765
|
+
return self.target_field.cast_db_type(connection=connection) # type: ignore[attr-defined]
|
744
766
|
|
745
|
-
def db_parameters(self, connection):
|
746
|
-
target_db_parameters = self.target_field.db_parameters(connection)
|
767
|
+
def db_parameters(self, connection: BaseDatabaseWrapper) -> dict[str, Any]:
|
768
|
+
target_db_parameters = self.target_field.db_parameters(connection) # type: ignore[attr-defined]
|
747
769
|
return {
|
748
770
|
"type": self.db_type(connection),
|
749
771
|
"check": self.db_check(connection),
|
750
772
|
"collation": target_db_parameters.get("collation"),
|
751
773
|
}
|
752
774
|
|
753
|
-
def get_col(self, alias, output_field=None):
|
775
|
+
def get_col(self, alias: str, output_field: Field | None = None) -> Any:
|
754
776
|
if output_field is None:
|
755
777
|
output_field = self.target_field
|
756
778
|
while isinstance(output_field, ForeignKey):
|
757
779
|
output_field = output_field.target_field
|
758
780
|
if output_field is self:
|
759
781
|
raise ValueError("Cannot resolve output_field.")
|
760
|
-
return super().get_col(alias, output_field)
|
782
|
+
return super().get_col(alias, output_field) # type: ignore[misc]
|
761
783
|
|
762
784
|
|
763
785
|
# Register lookups for ForeignKey
|
@@ -791,20 +813,20 @@ class ManyToManyField(RelatedField):
|
|
791
813
|
|
792
814
|
def __init__(
|
793
815
|
self,
|
794
|
-
to,
|
816
|
+
to: type[Model] | str,
|
795
817
|
*,
|
796
|
-
through,
|
797
|
-
through_fields=None,
|
798
|
-
related_name=None,
|
799
|
-
related_query_name=None,
|
800
|
-
limit_choices_to=None,
|
801
|
-
symmetrical=None,
|
802
|
-
**kwargs,
|
818
|
+
through: type[Model] | str,
|
819
|
+
through_fields: tuple[str, str] | None = None,
|
820
|
+
related_name: str | None = None,
|
821
|
+
related_query_name: str | None = None,
|
822
|
+
limit_choices_to: Any = None,
|
823
|
+
symmetrical: bool | None = None,
|
824
|
+
**kwargs: Any,
|
803
825
|
):
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
826
|
+
if not isinstance(to, str):
|
827
|
+
try:
|
828
|
+
to._model_meta
|
829
|
+
except AttributeError:
|
808
830
|
raise TypeError(
|
809
831
|
f"{self.__class__.__name__}({to!r}) is invalid. First parameter to ManyToManyField "
|
810
832
|
f"must be either a model, a model name, or the string {RECURSIVE_RELATIONSHIP_CONSTANT!r}"
|
@@ -835,18 +857,18 @@ class ManyToManyField(RelatedField):
|
|
835
857
|
**kwargs,
|
836
858
|
)
|
837
859
|
|
838
|
-
def preflight(self, **kwargs):
|
860
|
+
def preflight(self, **kwargs: Any) -> list[PreflightResult]: # type: ignore[misc]
|
839
861
|
return [
|
840
|
-
*super().preflight(**kwargs),
|
862
|
+
*super().preflight(**kwargs), # type: ignore[misc]
|
841
863
|
*self._check_relationship_model(**kwargs),
|
842
864
|
*self._check_ignored_options(**kwargs),
|
843
865
|
*self._check_table_uniqueness(**kwargs),
|
844
866
|
]
|
845
867
|
|
846
|
-
def _check_ignored_options(self, **kwargs):
|
847
|
-
warnings = []
|
868
|
+
def _check_ignored_options(self, **kwargs: Any) -> list[PreflightResult]:
|
869
|
+
warnings: list[PreflightResult] = []
|
848
870
|
|
849
|
-
if self.has_null_arg:
|
871
|
+
if self.has_null_arg: # type: ignore[attr-defined]
|
850
872
|
warnings.append(
|
851
873
|
PreflightResult(
|
852
874
|
fix="The 'null' option has no effect on ManyToManyField. Remove the 'null' argument.",
|
@@ -865,7 +887,7 @@ class ManyToManyField(RelatedField):
|
|
865
887
|
warning=True,
|
866
888
|
)
|
867
889
|
)
|
868
|
-
if self.remote_field.symmetrical and self._related_name:
|
890
|
+
if self.remote_field.symmetrical and self._related_name: # type: ignore[attr-defined]
|
869
891
|
warnings.append(
|
870
892
|
PreflightResult(
|
871
893
|
fix=(
|
@@ -878,7 +900,7 @@ class ManyToManyField(RelatedField):
|
|
878
900
|
warning=True,
|
879
901
|
)
|
880
902
|
)
|
881
|
-
if self.db_comment:
|
903
|
+
if self.db_comment: # type: ignore[attr-defined]
|
882
904
|
warnings.append(
|
883
905
|
PreflightResult(
|
884
906
|
fix="The 'db_comment' option has no effect on ManyToManyField. Remove the 'db_comment' argument.",
|
@@ -890,15 +912,17 @@ class ManyToManyField(RelatedField):
|
|
890
912
|
|
891
913
|
return warnings
|
892
914
|
|
893
|
-
def _check_relationship_model(
|
894
|
-
|
895
|
-
|
915
|
+
def _check_relationship_model(
|
916
|
+
self, from_model: type[Model] | None = None, **kwargs: Any
|
917
|
+
) -> list[PreflightResult]:
|
918
|
+
if hasattr(self.remote_field.through, "_model_meta"):
|
919
|
+
qualified_model_name = f"{self.remote_field.through.model_options.package_label}.{self.remote_field.through.__name__}"
|
896
920
|
else:
|
897
921
|
qualified_model_name = self.remote_field.through
|
898
922
|
|
899
923
|
errors = []
|
900
924
|
|
901
|
-
if self.remote_field.through not in self.
|
925
|
+
if self.remote_field.through not in self.meta.models_registry.get_models():
|
902
926
|
# The relationship model is not installed.
|
903
927
|
errors.append(
|
904
928
|
PreflightResult(
|
@@ -920,18 +944,20 @@ class ManyToManyField(RelatedField):
|
|
920
944
|
)
|
921
945
|
# Set some useful local variables
|
922
946
|
to_model = resolve_relation(from_model, self.remote_field.model)
|
923
|
-
from_model_name = from_model.
|
947
|
+
from_model_name = from_model.model_options.object_name
|
924
948
|
if isinstance(to_model, str):
|
925
949
|
to_model_name = to_model
|
926
950
|
else:
|
927
|
-
to_model_name = to_model.
|
928
|
-
relationship_model_name =
|
951
|
+
to_model_name = to_model.model_options.object_name
|
952
|
+
relationship_model_name = (
|
953
|
+
self.remote_field.through.model_options.object_name
|
954
|
+
)
|
929
955
|
self_referential = from_model == to_model
|
930
956
|
# Count foreign keys in intermediate model
|
931
957
|
if self_referential:
|
932
958
|
seen_self = sum(
|
933
959
|
from_model == getattr(field.remote_field, "model", None)
|
934
|
-
for field in self.remote_field.through.
|
960
|
+
for field in self.remote_field.through._model_meta.fields
|
935
961
|
)
|
936
962
|
|
937
963
|
if seen_self > 2 and not self.remote_field.through_fields:
|
@@ -953,11 +979,11 @@ class ManyToManyField(RelatedField):
|
|
953
979
|
# Count foreign keys in relationship model
|
954
980
|
seen_from = sum(
|
955
981
|
from_model == getattr(field.remote_field, "model", None)
|
956
|
-
for field in self.remote_field.through.
|
982
|
+
for field in self.remote_field.through._model_meta.fields
|
957
983
|
)
|
958
984
|
seen_to = sum(
|
959
985
|
to_model == getattr(field.remote_field, "model", None)
|
960
|
-
for field in self.remote_field.through.
|
986
|
+
for field in self.remote_field.through._model_meta.fields
|
961
987
|
)
|
962
988
|
|
963
989
|
if seen_from > 1 and not self.remote_field.through_fields:
|
@@ -1054,7 +1080,7 @@ class ManyToManyField(RelatedField):
|
|
1054
1080
|
(target_field_name, target),
|
1055
1081
|
):
|
1056
1082
|
possible_field_names = []
|
1057
|
-
for f in through.
|
1083
|
+
for f in through._model_meta.fields:
|
1058
1084
|
if (
|
1059
1085
|
hasattr(f, "remote_field")
|
1060
1086
|
and getattr(f.remote_field, "model", None) == related_model
|
@@ -1064,7 +1090,7 @@ class ManyToManyField(RelatedField):
|
|
1064
1090
|
fix = (
|
1065
1091
|
"Did you mean one of the following foreign keys to '{}': "
|
1066
1092
|
"{}?".format(
|
1067
|
-
related_model.
|
1093
|
+
related_model.model_options.object_name,
|
1068
1094
|
", ".join(possible_field_names),
|
1069
1095
|
)
|
1070
1096
|
)
|
@@ -1072,7 +1098,7 @@ class ManyToManyField(RelatedField):
|
|
1072
1098
|
fix = ""
|
1073
1099
|
|
1074
1100
|
try:
|
1075
|
-
field = through.
|
1101
|
+
field = through._model_meta.get_field(field_name)
|
1076
1102
|
except FieldDoesNotExist:
|
1077
1103
|
errors.append(
|
1078
1104
|
PreflightResult(
|
@@ -1089,7 +1115,7 @@ class ManyToManyField(RelatedField):
|
|
1089
1115
|
):
|
1090
1116
|
errors.append(
|
1091
1117
|
PreflightResult(
|
1092
|
-
fix=f"'{through.
|
1118
|
+
fix=f"'{through.model_options.object_name}.{field_name}' is not a foreign key to '{related_model.model_options.object_name}'. {fix}",
|
1093
1119
|
obj=self,
|
1094
1120
|
id="fields.m2m_through_field_not_fk_to_model",
|
1095
1121
|
)
|
@@ -1097,19 +1123,19 @@ class ManyToManyField(RelatedField):
|
|
1097
1123
|
|
1098
1124
|
return errors
|
1099
1125
|
|
1100
|
-
def _check_table_uniqueness(self, **kwargs):
|
1101
|
-
if isinstance(self.remote_field.through, str):
|
1126
|
+
def _check_table_uniqueness(self, **kwargs: Any) -> list[PreflightResult]:
|
1127
|
+
if isinstance(self.remote_field.through, str): # type: ignore[attr-defined]
|
1102
1128
|
return []
|
1103
1129
|
registered_tables = {
|
1104
|
-
model.
|
1105
|
-
for model in self.
|
1106
|
-
if model != self.remote_field.through
|
1130
|
+
model.model_options.db_table: model
|
1131
|
+
for model in self.meta.models_registry.get_models() # type: ignore[attr-defined]
|
1132
|
+
if model != self.remote_field.through # type: ignore[attr-defined]
|
1107
1133
|
}
|
1108
|
-
m2m_db_table = self.m2m_db_table()
|
1134
|
+
m2m_db_table = self.m2m_db_table() # type: ignore[attr-defined]
|
1109
1135
|
model = registered_tables.get(m2m_db_table)
|
1110
1136
|
# Check if there's already a m2m field using the same through model.
|
1111
|
-
if model and model != self.remote_field.through:
|
1112
|
-
clashing_obj = model.
|
1137
|
+
if model and model != self.remote_field.through: # type: ignore[attr-defined]
|
1138
|
+
clashing_obj = model.model_options.label
|
1113
1139
|
return [
|
1114
1140
|
PreflightResult(
|
1115
1141
|
fix=(
|
@@ -1123,71 +1149,73 @@ class ManyToManyField(RelatedField):
|
|
1123
1149
|
]
|
1124
1150
|
return []
|
1125
1151
|
|
1126
|
-
def deconstruct(self):
|
1152
|
+
def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
|
1127
1153
|
name, path, args, kwargs = super().deconstruct()
|
1128
1154
|
|
1129
|
-
if self.remote_field.db_constraint is not True:
|
1130
|
-
kwargs["db_constraint"] = self.remote_field.db_constraint
|
1155
|
+
if self.remote_field.db_constraint is not True: # type: ignore[attr-defined]
|
1156
|
+
kwargs["db_constraint"] = self.remote_field.db_constraint # type: ignore[attr-defined]
|
1131
1157
|
|
1132
1158
|
# Lowercase model names as they should be treated as case-insensitive.
|
1133
|
-
if isinstance(self.remote_field.model, str):
|
1134
|
-
if "." in self.remote_field.model:
|
1135
|
-
package_label, model_name = self.remote_field.model.split(".")
|
1159
|
+
if isinstance(self.remote_field.model, str): # type: ignore[attr-defined]
|
1160
|
+
if "." in self.remote_field.model: # type: ignore[attr-defined]
|
1161
|
+
package_label, model_name = self.remote_field.model.split(".") # type: ignore[attr-defined]
|
1136
1162
|
kwargs["to"] = f"{package_label}.{model_name.lower()}"
|
1137
1163
|
else:
|
1138
|
-
kwargs["to"] = self.remote_field.model.lower()
|
1164
|
+
kwargs["to"] = self.remote_field.model.lower() # type: ignore[attr-defined]
|
1139
1165
|
else:
|
1140
|
-
kwargs["to"] = self.remote_field.model.
|
1166
|
+
kwargs["to"] = self.remote_field.model.model_options.label_lower
|
1141
1167
|
|
1142
|
-
if isinstance(self.remote_field.through, str):
|
1143
|
-
kwargs["through"] = self.remote_field.through
|
1168
|
+
if isinstance(self.remote_field.through, str): # type: ignore[attr-defined]
|
1169
|
+
kwargs["through"] = self.remote_field.through # type: ignore[attr-defined]
|
1144
1170
|
else:
|
1145
|
-
kwargs["through"] = self.remote_field.through.
|
1171
|
+
kwargs["through"] = self.remote_field.through.model_options.label
|
1146
1172
|
|
1147
1173
|
return name, path, args, kwargs
|
1148
1174
|
|
1149
|
-
def _get_path_info(
|
1175
|
+
def _get_path_info(
|
1176
|
+
self, direct: bool = False, filtered_relation: Any = None
|
1177
|
+
) -> list[PathInfo]:
|
1150
1178
|
"""Called by both direct and indirect m2m traversal."""
|
1151
|
-
int_model = self.remote_field.through
|
1152
|
-
linkfield1 = int_model.
|
1153
|
-
linkfield2 = int_model.
|
1179
|
+
int_model = self.remote_field.through # type: ignore[attr-defined]
|
1180
|
+
linkfield1 = int_model._model_meta.get_field(self.m2m_field_name())
|
1181
|
+
linkfield2 = int_model._model_meta.get_field(self.m2m_reverse_field_name())
|
1154
1182
|
if direct:
|
1155
|
-
join1infos = linkfield1.reverse_path_infos
|
1183
|
+
join1infos = linkfield1.reverse_path_infos # type: ignore[attr-defined]
|
1156
1184
|
if filtered_relation:
|
1157
|
-
join2infos = linkfield2.get_path_info(filtered_relation)
|
1185
|
+
join2infos = linkfield2.get_path_info(filtered_relation) # type: ignore[attr-defined]
|
1158
1186
|
else:
|
1159
|
-
join2infos = linkfield2.path_infos
|
1187
|
+
join2infos = linkfield2.path_infos # type: ignore[attr-defined]
|
1160
1188
|
else:
|
1161
|
-
join1infos = linkfield2.reverse_path_infos
|
1189
|
+
join1infos = linkfield2.reverse_path_infos # type: ignore[attr-defined]
|
1162
1190
|
if filtered_relation:
|
1163
|
-
join2infos = linkfield1.get_path_info(filtered_relation)
|
1191
|
+
join2infos = linkfield1.get_path_info(filtered_relation) # type: ignore[attr-defined]
|
1164
1192
|
else:
|
1165
|
-
join2infos = linkfield1.path_infos
|
1193
|
+
join2infos = linkfield1.path_infos # type: ignore[attr-defined]
|
1166
1194
|
|
1167
1195
|
return [*join1infos, *join2infos]
|
1168
1196
|
|
1169
|
-
def get_path_info(self, filtered_relation=None):
|
1197
|
+
def get_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
|
1170
1198
|
return self._get_path_info(direct=True, filtered_relation=filtered_relation)
|
1171
1199
|
|
1172
1200
|
@cached_property
|
1173
|
-
def path_infos(self):
|
1201
|
+
def path_infos(self) -> list[PathInfo]:
|
1174
1202
|
return self.get_path_info()
|
1175
1203
|
|
1176
|
-
def get_reverse_path_info(self, filtered_relation=None):
|
1204
|
+
def get_reverse_path_info(self, filtered_relation: Any = None) -> list[PathInfo]:
|
1177
1205
|
return self._get_path_info(direct=False, filtered_relation=filtered_relation)
|
1178
1206
|
|
1179
1207
|
@cached_property
|
1180
|
-
def reverse_path_infos(self):
|
1208
|
+
def reverse_path_infos(self) -> list[PathInfo]:
|
1181
1209
|
return self.get_reverse_path_info()
|
1182
1210
|
|
1183
|
-
def _get_m2m_db_table(self):
|
1211
|
+
def _get_m2m_db_table(self) -> str:
|
1184
1212
|
"""
|
1185
1213
|
Function that can be curried to provide the m2m table name for this
|
1186
1214
|
relation.
|
1187
1215
|
"""
|
1188
|
-
return self.remote_field.through.
|
1216
|
+
return self.remote_field.through.model_options.db_table
|
1189
1217
|
|
1190
|
-
def _get_m2m_attr(self, related, attr):
|
1218
|
+
def _get_m2m_attr(self, related: Any, attr: str) -> Any:
|
1191
1219
|
"""
|
1192
1220
|
Function that can be curried to provide the source accessor or DB
|
1193
1221
|
column name for the m2m table.
|
@@ -1195,20 +1223,21 @@ class ManyToManyField(RelatedField):
|
|
1195
1223
|
cache_attr = f"_m2m_{attr}_cache"
|
1196
1224
|
if hasattr(self, cache_attr):
|
1197
1225
|
return getattr(self, cache_attr)
|
1198
|
-
if self.remote_field.through_fields is not None:
|
1199
|
-
link_field_name = self.remote_field.through_fields[0]
|
1226
|
+
if self.remote_field.through_fields is not None: # type: ignore[attr-defined]
|
1227
|
+
link_field_name: str | None = self.remote_field.through_fields[0] # type: ignore[attr-defined]
|
1200
1228
|
else:
|
1201
1229
|
link_field_name = None
|
1202
|
-
for f in self.remote_field.through.
|
1230
|
+
for f in self.remote_field.through._model_meta.fields:
|
1203
1231
|
if (
|
1204
|
-
f.is_relation
|
1205
|
-
and f.remote_field.model == related.related_model
|
1232
|
+
f.is_relation # type: ignore[attr-defined]
|
1233
|
+
and f.remote_field.model == related.related_model # type: ignore[attr-defined]
|
1206
1234
|
and (link_field_name is None or link_field_name == f.name)
|
1207
1235
|
):
|
1208
1236
|
setattr(self, cache_attr, getattr(f, attr))
|
1209
1237
|
return getattr(self, cache_attr)
|
1238
|
+
return None
|
1210
1239
|
|
1211
|
-
def _get_m2m_reverse_attr(self, related, attr):
|
1240
|
+
def _get_m2m_reverse_attr(self, related: Any, attr: str) -> Any:
|
1212
1241
|
"""
|
1213
1242
|
Function that can be curried to provide the related accessor or DB
|
1214
1243
|
column name for the m2m table.
|
@@ -1217,12 +1246,12 @@ class ManyToManyField(RelatedField):
|
|
1217
1246
|
if hasattr(self, cache_attr):
|
1218
1247
|
return getattr(self, cache_attr)
|
1219
1248
|
found = False
|
1220
|
-
if self.remote_field.through_fields is not None:
|
1221
|
-
link_field_name = self.remote_field.through_fields[1]
|
1249
|
+
if self.remote_field.through_fields is not None: # type: ignore[attr-defined]
|
1250
|
+
link_field_name: str | None = self.remote_field.through_fields[1] # type: ignore[attr-defined]
|
1222
1251
|
else:
|
1223
1252
|
link_field_name = None
|
1224
|
-
for f in self.remote_field.through.
|
1225
|
-
if f.is_relation and f.remote_field.model == related.model:
|
1253
|
+
for f in self.remote_field.through._model_meta.fields:
|
1254
|
+
if f.is_relation and f.remote_field.model == related.model: # type: ignore[attr-defined]
|
1226
1255
|
if link_field_name is None and related.related_model == related.model:
|
1227
1256
|
# If this is an m2m-intermediate to self,
|
1228
1257
|
# the first foreign key you find will be
|
@@ -1238,64 +1267,69 @@ class ManyToManyField(RelatedField):
|
|
1238
1267
|
break
|
1239
1268
|
return getattr(self, cache_attr)
|
1240
1269
|
|
1241
|
-
def contribute_to_class(self, cls, name):
|
1270
|
+
def contribute_to_class(self, cls: type[Model], name: str) -> None:
|
1242
1271
|
super().contribute_to_class(cls, name)
|
1243
1272
|
|
1244
|
-
def resolve_through_model(
|
1245
|
-
|
1273
|
+
def resolve_through_model(
|
1274
|
+
_: Any, model: type[Model], field: ManyToManyField
|
1275
|
+
) -> None:
|
1276
|
+
field.remote_field.through = model # type: ignore[attr-defined]
|
1246
1277
|
|
1247
1278
|
lazy_related_operation(
|
1248
|
-
resolve_through_model,
|
1279
|
+
resolve_through_model,
|
1280
|
+
cls,
|
1281
|
+
self.remote_field.through,
|
1282
|
+
field=self, # type: ignore[attr-defined]
|
1249
1283
|
)
|
1250
1284
|
|
1251
1285
|
# Add the descriptor for the m2m relation.
|
1252
|
-
setattr(cls, self.name, ForwardManyToManyDescriptor(self.remote_field))
|
1286
|
+
setattr(cls, self.name, ForwardManyToManyDescriptor(self.remote_field)) # type: ignore[attr-defined]
|
1253
1287
|
|
1254
1288
|
# Set up the accessor for the m2m table name for the relation.
|
1255
|
-
self.m2m_db_table = self._get_m2m_db_table
|
1289
|
+
self.m2m_db_table = self._get_m2m_db_table # type: ignore[method-assign]
|
1256
1290
|
|
1257
|
-
def contribute_to_related_class(self, cls, related):
|
1291
|
+
def contribute_to_related_class(self, cls: type[Model], related: Any) -> None:
|
1258
1292
|
# Internal M2Ms (i.e., those with a related name ending with '+')
|
1259
1293
|
# don't get a related descriptor.
|
1260
|
-
if not self.remote_field.is_hidden():
|
1294
|
+
if not self.remote_field.is_hidden(): # type: ignore[attr-defined]
|
1261
1295
|
setattr(
|
1262
1296
|
cls,
|
1263
1297
|
related.get_accessor_name(),
|
1264
|
-
ReverseManyToManyDescriptor(self.remote_field),
|
1298
|
+
ReverseManyToManyDescriptor(self.remote_field), # type: ignore[attr-defined]
|
1265
1299
|
)
|
1266
1300
|
|
1267
1301
|
# Set up the accessors for the column names on the m2m table.
|
1268
|
-
self.m2m_column_name = partial(self._get_m2m_attr, related, "column")
|
1269
|
-
self.m2m_reverse_name = partial(self._get_m2m_reverse_attr, related, "column")
|
1302
|
+
self.m2m_column_name = partial(self._get_m2m_attr, related, "column") # type: ignore[method-assign]
|
1303
|
+
self.m2m_reverse_name = partial(self._get_m2m_reverse_attr, related, "column") # type: ignore[method-assign]
|
1270
1304
|
|
1271
|
-
self.m2m_field_name = partial(self._get_m2m_attr, related, "name")
|
1272
|
-
self.m2m_reverse_field_name = partial(
|
1305
|
+
self.m2m_field_name = partial(self._get_m2m_attr, related, "name") # type: ignore[method-assign]
|
1306
|
+
self.m2m_reverse_field_name = partial( # type: ignore[method-assign]
|
1273
1307
|
self._get_m2m_reverse_attr, related, "name"
|
1274
1308
|
)
|
1275
1309
|
|
1276
1310
|
get_m2m_rel = partial(self._get_m2m_attr, related, "remote_field")
|
1277
|
-
self.m2m_target_field_name = lambda: get_m2m_rel().field_name
|
1311
|
+
self.m2m_target_field_name = lambda: get_m2m_rel().field_name # type: ignore[method-assign,attr-defined]
|
1278
1312
|
get_m2m_reverse_rel = partial(
|
1279
1313
|
self._get_m2m_reverse_attr, related, "remote_field"
|
1280
1314
|
)
|
1281
|
-
self.m2m_reverse_target_field_name = lambda: get_m2m_reverse_rel().field_name
|
1315
|
+
self.m2m_reverse_target_field_name = lambda: get_m2m_reverse_rel().field_name # type: ignore[method-assign,attr-defined]
|
1282
1316
|
|
1283
|
-
def set_attributes_from_rel(self):
|
1317
|
+
def set_attributes_from_rel(self) -> None:
|
1284
1318
|
pass
|
1285
1319
|
|
1286
|
-
def value_from_object(self, obj):
|
1287
|
-
return [] if obj.id is None else list(getattr(obj, self.attname).all())
|
1320
|
+
def value_from_object(self, obj: Model) -> list[Any]:
|
1321
|
+
return [] if obj.id is None else list(getattr(obj, self.attname).all()) # type: ignore[attr-defined]
|
1288
1322
|
|
1289
|
-
def save_form_data(self, instance, data):
|
1290
|
-
getattr(instance, self.attname).set(data)
|
1323
|
+
def save_form_data(self, instance: Model, data: Any) -> None:
|
1324
|
+
getattr(instance, self.attname).set(data) # type: ignore[attr-defined]
|
1291
1325
|
|
1292
|
-
def db_check(self, connection):
|
1326
|
+
def db_check(self, connection: BaseDatabaseWrapper) -> None:
|
1293
1327
|
return None
|
1294
1328
|
|
1295
|
-
def db_type(self, connection):
|
1329
|
+
def db_type(self, connection: BaseDatabaseWrapper) -> None:
|
1296
1330
|
# A ManyToManyField is not represented by a single column,
|
1297
1331
|
# so return None.
|
1298
1332
|
return None
|
1299
1333
|
|
1300
|
-
def db_parameters(self, connection):
|
1334
|
+
def db_parameters(self, connection: BaseDatabaseWrapper) -> dict[str, None]:
|
1301
1335
|
return {"type": None, "check": None}
|