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/query_utils.py
CHANGED
@@ -6,28 +6,39 @@ large and/or so that they can be used by other modules without getting into
|
|
6
6
|
circular import difficulties.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import functools
|
10
12
|
import inspect
|
11
13
|
import logging
|
12
14
|
from collections import namedtuple
|
15
|
+
from collections.abc import Generator
|
16
|
+
from typing import TYPE_CHECKING, Any
|
13
17
|
|
14
18
|
from plain.models.constants import LOOKUP_SEP
|
15
19
|
from plain.models.db import DatabaseError, db_connection
|
16
20
|
from plain.models.exceptions import FieldError
|
17
21
|
from plain.utils import tree
|
18
22
|
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
25
|
+
from plain.models.base import Model
|
26
|
+
from plain.models.fields import Field
|
27
|
+
from plain.models.meta import Meta
|
28
|
+
from plain.models.sql.compiler import SQLCompiler
|
29
|
+
|
19
30
|
logger = logging.getLogger("plain.models")
|
20
31
|
|
21
32
|
# PathInfo is used when converting lookups (fk__somecol). The contents
|
22
|
-
# describe the relation in Model terms (
|
23
|
-
# sides of the relation. The join_field is the field backing the relation.
|
33
|
+
# describe the relation in Model terms (Meta and Fields for both
|
34
|
+
# sides of the relation). The join_field is the field backing the relation.
|
24
35
|
PathInfo = namedtuple(
|
25
36
|
"PathInfo",
|
26
|
-
"
|
37
|
+
"from_meta to_meta target_fields join_field m2m direct filtered_relation",
|
27
38
|
)
|
28
39
|
|
29
40
|
|
30
|
-
def subclasses(cls):
|
41
|
+
def subclasses(cls: type) -> Generator[type, None, None]:
|
31
42
|
yield cls
|
32
43
|
for subclass in cls.__subclasses__():
|
33
44
|
yield from subclasses(subclass)
|
@@ -46,14 +57,20 @@ class Q(tree.Node):
|
|
46
57
|
default = AND
|
47
58
|
conditional = True
|
48
59
|
|
49
|
-
def __init__(
|
60
|
+
def __init__(
|
61
|
+
self,
|
62
|
+
*args: Any,
|
63
|
+
_connector: str | None = None,
|
64
|
+
_negated: bool = False,
|
65
|
+
**kwargs: Any,
|
66
|
+
) -> None:
|
50
67
|
super().__init__(
|
51
68
|
children=[*args, *sorted(kwargs.items())],
|
52
69
|
connector=_connector,
|
53
70
|
negated=_negated,
|
54
71
|
)
|
55
72
|
|
56
|
-
def _combine(self, other, conn):
|
73
|
+
def _combine(self, other: Any, conn: str) -> Q:
|
57
74
|
if getattr(other, "conditional", False) is False:
|
58
75
|
raise TypeError(other)
|
59
76
|
if not self:
|
@@ -66,26 +83,31 @@ class Q(tree.Node):
|
|
66
83
|
obj.add(other, conn)
|
67
84
|
return obj
|
68
85
|
|
69
|
-
def __or__(self, other):
|
86
|
+
def __or__(self, other: Any) -> Q:
|
70
87
|
return self._combine(other, self.OR)
|
71
88
|
|
72
|
-
def __and__(self, other):
|
89
|
+
def __and__(self, other: Any) -> Q:
|
73
90
|
return self._combine(other, self.AND)
|
74
91
|
|
75
|
-
def __xor__(self, other):
|
92
|
+
def __xor__(self, other: Any) -> Q:
|
76
93
|
return self._combine(other, self.XOR)
|
77
94
|
|
78
|
-
def __invert__(self):
|
95
|
+
def __invert__(self) -> Q:
|
79
96
|
obj = self.copy()
|
80
97
|
obj.negate()
|
81
98
|
return obj
|
82
99
|
|
83
100
|
def resolve_expression(
|
84
|
-
self,
|
85
|
-
|
101
|
+
self,
|
102
|
+
query: Any = None,
|
103
|
+
allow_joins: bool = True,
|
104
|
+
reuse: Any = None,
|
105
|
+
summarize: bool = False,
|
106
|
+
for_save: bool = False,
|
107
|
+
) -> Any:
|
86
108
|
# We must promote any new joins to left outer joins so that when Q is
|
87
109
|
# used as an expression, rows aren't filtered due to joins.
|
88
|
-
clause, joins = query._add_q(
|
110
|
+
clause, joins = query._add_q( # type: ignore[union-attr]
|
89
111
|
self,
|
90
112
|
reuse,
|
91
113
|
allow_joins=allow_joins,
|
@@ -93,10 +115,10 @@ class Q(tree.Node):
|
|
93
115
|
check_filterable=False,
|
94
116
|
summarize=summarize,
|
95
117
|
)
|
96
|
-
query.promote_joins(joins)
|
118
|
+
query.promote_joins(joins) # type: ignore[union-attr]
|
97
119
|
return clause
|
98
120
|
|
99
|
-
def flatten(self):
|
121
|
+
def flatten(self) -> Generator[Any, None, None]:
|
100
122
|
"""
|
101
123
|
Recursively yield this Q object and all subexpressions, in depth-first
|
102
124
|
order.
|
@@ -111,7 +133,7 @@ class Q(tree.Node):
|
|
111
133
|
else:
|
112
134
|
yield child
|
113
135
|
|
114
|
-
def check(self, against):
|
136
|
+
def check(self, against: dict[str, Any]) -> bool:
|
115
137
|
"""
|
116
138
|
Do a database query to check if the expressions of the Q instance
|
117
139
|
matches against the expressions.
|
@@ -123,7 +145,7 @@ class Q(tree.Node):
|
|
123
145
|
from plain.models.sql import Query
|
124
146
|
from plain.models.sql.constants import SINGLE
|
125
147
|
|
126
|
-
query = Query(None)
|
148
|
+
query = Query(None) # type: ignore[arg-type]
|
127
149
|
for name, value in against.items():
|
128
150
|
if not hasattr(value, "resolve_expression"):
|
129
151
|
value = Value(value)
|
@@ -141,12 +163,12 @@ class Q(tree.Node):
|
|
141
163
|
logger.warning("Got a database error calling check() on %r: %s", self, e)
|
142
164
|
return True
|
143
165
|
|
144
|
-
def deconstruct(self):
|
166
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
145
167
|
path = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
146
168
|
if path.startswith("plain.models.query_utils"):
|
147
169
|
path = path.replace("plain.models.query_utils", "plain.models")
|
148
170
|
args = tuple(self.children)
|
149
|
-
kwargs = {}
|
171
|
+
kwargs: dict[str, Any] = {}
|
150
172
|
if self.connector != self.default:
|
151
173
|
kwargs["_connector"] = self.connector
|
152
174
|
if self.negated:
|
@@ -160,10 +182,10 @@ class DeferredAttribute:
|
|
160
182
|
object the first time, the query is executed.
|
161
183
|
"""
|
162
184
|
|
163
|
-
def __init__(self, field):
|
185
|
+
def __init__(self, field: Any) -> None:
|
164
186
|
self.field = field
|
165
187
|
|
166
|
-
def __get__(self, instance, cls=None):
|
188
|
+
def __get__(self, instance: Any, cls: type | None = None) -> Any:
|
167
189
|
"""
|
168
190
|
Retrieve and caches the value from the datastore on the first lookup.
|
169
191
|
Return the cached value.
|
@@ -183,37 +205,37 @@ class class_or_instance_method:
|
|
183
205
|
the caller type (instance or class of models.Field).
|
184
206
|
"""
|
185
207
|
|
186
|
-
def __init__(self, class_method, instance_method):
|
208
|
+
def __init__(self, class_method: Any, instance_method: Any) -> None:
|
187
209
|
self.class_method = class_method
|
188
210
|
self.instance_method = instance_method
|
189
211
|
|
190
|
-
def __get__(self, instance, owner):
|
212
|
+
def __get__(self, instance: Any, owner: type) -> Any:
|
191
213
|
if instance is None:
|
192
214
|
return functools.partial(self.class_method, owner)
|
193
215
|
return functools.partial(self.instance_method, instance)
|
194
216
|
|
195
217
|
|
196
218
|
class RegisterLookupMixin:
|
197
|
-
def _get_lookup(self, lookup_name):
|
219
|
+
def _get_lookup(self, lookup_name: str) -> type | None:
|
198
220
|
return self.get_lookups().get(lookup_name, None)
|
199
221
|
|
200
222
|
@functools.cache
|
201
|
-
def get_class_lookups(cls):
|
223
|
+
def get_class_lookups(cls: type) -> dict[str, type]:
|
202
224
|
class_lookups = [
|
203
225
|
parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
|
204
226
|
]
|
205
|
-
return cls.merge_dicts(class_lookups)
|
227
|
+
return cls.merge_dicts(class_lookups) # type: ignore[attr-defined]
|
206
228
|
|
207
|
-
def get_instance_lookups(self):
|
229
|
+
def get_instance_lookups(self) -> dict[str, type]:
|
208
230
|
class_lookups = self.get_class_lookups()
|
209
231
|
if instance_lookups := getattr(self, "instance_lookups", None):
|
210
232
|
return {**class_lookups, **instance_lookups}
|
211
233
|
return class_lookups
|
212
234
|
|
213
235
|
get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
|
214
|
-
get_class_lookups = classmethod(get_class_lookups)
|
236
|
+
get_class_lookups = classmethod(get_class_lookups) # type: ignore[assignment]
|
215
237
|
|
216
|
-
def get_lookup(self, lookup_name):
|
238
|
+
def get_lookup(self, lookup_name: str) -> type | None:
|
217
239
|
from plain.models.lookups import Lookup
|
218
240
|
|
219
241
|
found = self._get_lookup(lookup_name)
|
@@ -223,7 +245,7 @@ class RegisterLookupMixin:
|
|
223
245
|
return None
|
224
246
|
return found
|
225
247
|
|
226
|
-
def get_transform(self, lookup_name):
|
248
|
+
def get_transform(self, lookup_name: str) -> type | None:
|
227
249
|
from plain.models.lookups import Transform
|
228
250
|
|
229
251
|
found = self._get_lookup(lookup_name)
|
@@ -234,33 +256,37 @@ class RegisterLookupMixin:
|
|
234
256
|
return found
|
235
257
|
|
236
258
|
@staticmethod
|
237
|
-
def merge_dicts(dicts):
|
259
|
+
def merge_dicts(dicts: list[dict[str, type]]) -> dict[str, type]:
|
238
260
|
"""
|
239
261
|
Merge dicts in reverse to preference the order of the original list. e.g.,
|
240
262
|
merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
|
241
263
|
"""
|
242
|
-
merged = {}
|
264
|
+
merged: dict[str, type] = {}
|
243
265
|
for d in reversed(dicts):
|
244
266
|
merged.update(d)
|
245
267
|
return merged
|
246
268
|
|
247
269
|
@classmethod
|
248
|
-
def _clear_cached_class_lookups(cls):
|
270
|
+
def _clear_cached_class_lookups(cls) -> None:
|
249
271
|
for subclass in subclasses(cls):
|
250
|
-
subclass.get_class_lookups.cache_clear()
|
272
|
+
subclass.get_class_lookups.cache_clear() # type: ignore[attr-defined]
|
251
273
|
|
252
|
-
def register_class_lookup(
|
274
|
+
def register_class_lookup(
|
275
|
+
cls: type, lookup: type, lookup_name: str | None = None
|
276
|
+
) -> type:
|
253
277
|
if lookup_name is None:
|
254
|
-
lookup_name = lookup.lookup_name
|
278
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
255
279
|
if "class_lookups" not in cls.__dict__:
|
256
|
-
cls.class_lookups = {}
|
257
|
-
cls.class_lookups[lookup_name] = lookup
|
258
|
-
cls._clear_cached_class_lookups()
|
280
|
+
cls.class_lookups = {} # type: ignore[attr-defined]
|
281
|
+
cls.class_lookups[lookup_name] = lookup # type: ignore[attr-defined]
|
282
|
+
cls._clear_cached_class_lookups() # type: ignore[attr-defined]
|
259
283
|
return lookup
|
260
284
|
|
261
|
-
def register_instance_lookup(
|
285
|
+
def register_instance_lookup(
|
286
|
+
self, lookup: type, lookup_name: str | None = None
|
287
|
+
) -> type:
|
262
288
|
if lookup_name is None:
|
263
|
-
lookup_name = lookup.lookup_name
|
289
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
264
290
|
if "instance_lookups" not in self.__dict__:
|
265
291
|
self.instance_lookups = {}
|
266
292
|
self.instance_lookups[lookup_name] = lookup
|
@@ -269,34 +295,44 @@ class RegisterLookupMixin:
|
|
269
295
|
register_lookup = class_or_instance_method(
|
270
296
|
register_class_lookup, register_instance_lookup
|
271
297
|
)
|
272
|
-
register_class_lookup = classmethod(register_class_lookup)
|
298
|
+
register_class_lookup = classmethod(register_class_lookup) # type: ignore[assignment]
|
273
299
|
|
274
|
-
def _unregister_class_lookup(
|
300
|
+
def _unregister_class_lookup(
|
301
|
+
cls: type, lookup: type, lookup_name: str | None = None
|
302
|
+
) -> None:
|
275
303
|
"""
|
276
304
|
Remove given lookup from cls lookups. For use in tests only as it's
|
277
305
|
not thread-safe.
|
278
306
|
"""
|
279
307
|
if lookup_name is None:
|
280
|
-
lookup_name = lookup.lookup_name
|
281
|
-
del cls.class_lookups[lookup_name]
|
282
|
-
cls._clear_cached_class_lookups()
|
308
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
309
|
+
del cls.class_lookups[lookup_name] # type: ignore[attr-defined]
|
310
|
+
cls._clear_cached_class_lookups() # type: ignore[attr-defined]
|
283
311
|
|
284
|
-
def _unregister_instance_lookup(
|
312
|
+
def _unregister_instance_lookup(
|
313
|
+
self, lookup: type, lookup_name: str | None = None
|
314
|
+
) -> None:
|
285
315
|
"""
|
286
316
|
Remove given lookup from instance lookups. For use in tests only as
|
287
317
|
it's not thread-safe.
|
288
318
|
"""
|
289
319
|
if lookup_name is None:
|
290
|
-
lookup_name = lookup.lookup_name
|
320
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
291
321
|
del self.instance_lookups[lookup_name]
|
292
322
|
|
293
323
|
_unregister_lookup = class_or_instance_method(
|
294
324
|
_unregister_class_lookup, _unregister_instance_lookup
|
295
325
|
)
|
296
|
-
_unregister_class_lookup = classmethod(_unregister_class_lookup)
|
326
|
+
_unregister_class_lookup = classmethod(_unregister_class_lookup) # type: ignore[assignment]
|
297
327
|
|
298
328
|
|
299
|
-
def select_related_descend(
|
329
|
+
def select_related_descend(
|
330
|
+
field: Any,
|
331
|
+
restricted: bool,
|
332
|
+
requested: dict[str, Any],
|
333
|
+
select_mask: Any,
|
334
|
+
reverse: bool = False,
|
335
|
+
) -> bool:
|
300
336
|
"""
|
301
337
|
Return True if this field should be used to descend deeper for
|
302
338
|
select_related() purposes. Used by both the query construction code
|
@@ -327,13 +363,15 @@ def select_related_descend(field, restricted, requested, select_mask, reverse=Fa
|
|
327
363
|
and field not in select_mask
|
328
364
|
):
|
329
365
|
raise FieldError(
|
330
|
-
f"Field {field.model.
|
366
|
+
f"Field {field.model.model_options.object_name}.{field.name} cannot be both "
|
331
367
|
"deferred and traversed using select_related at the same time."
|
332
368
|
)
|
333
369
|
return True
|
334
370
|
|
335
371
|
|
336
|
-
def refs_expression(
|
372
|
+
def refs_expression(
|
373
|
+
lookup_parts: list[str], annotations: dict[str, Any]
|
374
|
+
) -> tuple[str | None, tuple[str, ...]]:
|
337
375
|
"""
|
338
376
|
Check if the lookup_parts contains references to the given annotations set.
|
339
377
|
Because the LOOKUP_SEP is contained in the default annotation names, check
|
@@ -342,20 +380,22 @@ def refs_expression(lookup_parts, annotations):
|
|
342
380
|
for n in range(1, len(lookup_parts) + 1):
|
343
381
|
level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
|
344
382
|
if annotations.get(level_n_lookup):
|
345
|
-
return level_n_lookup, lookup_parts[n:]
|
383
|
+
return level_n_lookup, tuple(lookup_parts[n:])
|
346
384
|
return None, ()
|
347
385
|
|
348
386
|
|
349
|
-
def check_rel_lookup_compatibility(
|
387
|
+
def check_rel_lookup_compatibility(
|
388
|
+
model: type[Model], target_meta: Meta, field: Field
|
389
|
+
) -> bool:
|
350
390
|
"""
|
351
|
-
Check that
|
391
|
+
Check that model is compatible with target_meta. Compatibility
|
352
392
|
is OK if:
|
353
|
-
1) model and
|
354
|
-
2) model is parent of
|
393
|
+
1) model and meta.model match (where proxy inheritance is removed)
|
394
|
+
2) model is parent of meta's model or the other way around
|
355
395
|
"""
|
356
396
|
|
357
|
-
def check(
|
358
|
-
return model ==
|
397
|
+
def check(meta: Meta) -> bool:
|
398
|
+
return model == meta.model
|
359
399
|
|
360
400
|
# If the field is a primary key, then doing a query against the field's
|
361
401
|
# model is ok, too. Consider the case:
|
@@ -363,28 +403,28 @@ def check_rel_lookup_compatibility(model, target_opts, field):
|
|
363
403
|
# place = OneToOneField(Place, primary_key=True):
|
364
404
|
# Restaurant.query.filter(id__in=Restaurant.query.all()).
|
365
405
|
# If we didn't have the primary key check, then id__in (== place__in) would
|
366
|
-
# give Place's
|
406
|
+
# give Place's meta as the target meta, but Restaurant isn't compatible
|
367
407
|
# with that. This logic applies only to primary keys, as when doing __in=qs,
|
368
408
|
# we are going to turn this into __in=qs.values('id') later on.
|
369
|
-
return check(
|
370
|
-
getattr(field, "primary_key", False) and check(field.model.
|
409
|
+
return check(target_meta) or (
|
410
|
+
getattr(field, "primary_key", False) and check(field.model._model_meta)
|
371
411
|
)
|
372
412
|
|
373
413
|
|
374
414
|
class FilteredRelation:
|
375
415
|
"""Specify custom filtering in the ON clause of SQL joins."""
|
376
416
|
|
377
|
-
def __init__(self, relation_name, *, condition=Q()):
|
417
|
+
def __init__(self, relation_name: str, *, condition: Q = Q()) -> None:
|
378
418
|
if not relation_name:
|
379
419
|
raise ValueError("relation_name cannot be empty.")
|
380
420
|
self.relation_name = relation_name
|
381
|
-
self.alias = None
|
421
|
+
self.alias: str | None = None
|
382
422
|
if not isinstance(condition, Q):
|
383
423
|
raise ValueError("condition argument must be a Q() instance.")
|
384
424
|
self.condition = condition
|
385
|
-
self.path = []
|
425
|
+
self.path: list[str] = []
|
386
426
|
|
387
|
-
def __eq__(self, other):
|
427
|
+
def __eq__(self, other: object) -> bool:
|
388
428
|
if not isinstance(other, self.__class__):
|
389
429
|
return NotImplemented
|
390
430
|
return (
|
@@ -393,20 +433,20 @@ class FilteredRelation:
|
|
393
433
|
and self.condition == other.condition
|
394
434
|
)
|
395
435
|
|
396
|
-
def clone(self):
|
436
|
+
def clone(self) -> FilteredRelation:
|
397
437
|
clone = FilteredRelation(self.relation_name, condition=self.condition)
|
398
438
|
clone.alias = self.alias
|
399
439
|
clone.path = self.path[:]
|
400
440
|
return clone
|
401
441
|
|
402
|
-
def resolve_expression(self, *args, **kwargs):
|
442
|
+
def resolve_expression(self, *args: Any, **kwargs: Any) -> Any:
|
403
443
|
"""
|
404
444
|
QuerySet.annotate() only accepts expression-like arguments
|
405
445
|
(with a resolve_expression() method).
|
406
446
|
"""
|
407
447
|
raise NotImplementedError("FilteredRelation.resolve_expression() is unused.")
|
408
448
|
|
409
|
-
def as_sql(self, compiler, connection):
|
449
|
+
def as_sql(self, compiler: SQLCompiler, connection: BaseDatabaseWrapper) -> Any:
|
410
450
|
# Resolve the condition in Join.filtered_relation.
|
411
451
|
query = compiler.query
|
412
452
|
where = query.build_filtered_relation_q(self.condition, reuse=set(self.path))
|
plain/models/registry.py
CHANGED
@@ -1,7 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import functools
|
2
4
|
import warnings
|
3
5
|
from collections import defaultdict
|
6
|
+
from collections.abc import Callable
|
4
7
|
from functools import partial
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from plain.models.base import Model
|
12
|
+
|
13
|
+
M = TypeVar("M", bound="Model")
|
5
14
|
|
6
15
|
|
7
16
|
class ModelsRegistryNotReady(Exception):
|
@@ -11,7 +20,7 @@ class ModelsRegistryNotReady(Exception):
|
|
11
20
|
|
12
21
|
|
13
22
|
class ModelsRegistry:
|
14
|
-
def __init__(self):
|
23
|
+
def __init__(self) -> None:
|
15
24
|
# Mapping of app labels => model names => model classes. Every time a
|
16
25
|
# model is imported, ModelBase.__new__ calls packages.register_model which
|
17
26
|
# creates an entry in all_models. All imported models are registered,
|
@@ -19,23 +28,25 @@ class ModelsRegistry:
|
|
19
28
|
# and whether the registry has been populated. Since it isn't possible
|
20
29
|
# to reimport a module safely (it could reexecute initialization code)
|
21
30
|
# all_models is never overridden or reset.
|
22
|
-
self.all_models = defaultdict(dict)
|
31
|
+
self.all_models: defaultdict[str, dict[str, type[Model]]] = defaultdict(dict)
|
23
32
|
|
24
33
|
# Maps ("package_label", "modelname") tuples to lists of functions to be
|
25
34
|
# called when the corresponding model is ready. Used by this class's
|
26
35
|
# `lazy_model_operation()` and `do_pending_operations()` methods.
|
27
|
-
self._pending_operations
|
36
|
+
self._pending_operations: defaultdict[
|
37
|
+
tuple[str, str], list[Callable[[type[Model]], None]]
|
38
|
+
] = defaultdict(list)
|
28
39
|
|
29
|
-
self.ready = False
|
40
|
+
self.ready: bool = False
|
30
41
|
|
31
|
-
def check_ready(self):
|
42
|
+
def check_ready(self) -> None:
|
32
43
|
"""Raise an exception if all models haven't been imported yet."""
|
33
44
|
if not self.ready:
|
34
45
|
raise ModelsRegistryNotReady("Models aren't loaded yet.")
|
35
46
|
|
36
47
|
# This method is performance-critical at least for Plain's test suite.
|
37
48
|
@functools.cache
|
38
|
-
def get_models(self, *, package_label=""):
|
49
|
+
def get_models(self, *, package_label: str = "") -> list[type[Model]]:
|
39
50
|
"""
|
40
51
|
Return a list of all installed models.
|
41
52
|
|
@@ -65,7 +76,12 @@ class ModelsRegistry:
|
|
65
76
|
|
66
77
|
return models
|
67
78
|
|
68
|
-
def get_model(
|
79
|
+
def get_model(
|
80
|
+
self,
|
81
|
+
package_label: str,
|
82
|
+
model_name: str | None = None,
|
83
|
+
require_ready: bool = True,
|
84
|
+
) -> type[Model]:
|
69
85
|
"""
|
70
86
|
Return the model matching the given package_label and model_name.
|
71
87
|
|
@@ -87,11 +103,11 @@ class ModelsRegistry:
|
|
87
103
|
package_models = self.all_models[package_label]
|
88
104
|
return package_models[model_name.lower()]
|
89
105
|
|
90
|
-
def register_model(self, package_label, model):
|
106
|
+
def register_model(self, package_label: str, model: type[Model]) -> None:
|
91
107
|
# Since this method is called when models are imported, it cannot
|
92
108
|
# perform imports because of the risk of import loops. It mustn't
|
93
109
|
# call get_package_config().
|
94
|
-
model_name = model.
|
110
|
+
model_name = model.model_options.model_name
|
95
111
|
app_models = self.all_models[package_label]
|
96
112
|
if model_name in app_models:
|
97
113
|
if (
|
@@ -113,7 +129,7 @@ class ModelsRegistry:
|
|
113
129
|
self.do_pending_operations(model)
|
114
130
|
self.clear_cache()
|
115
131
|
|
116
|
-
def _get_registered_model(self, package_label, model_name):
|
132
|
+
def _get_registered_model(self, package_label: str, model_name: str) -> type[Model]:
|
117
133
|
"""
|
118
134
|
Similar to get_model(), but doesn't require that an app exists with
|
119
135
|
the given package_label.
|
@@ -126,7 +142,7 @@ class ModelsRegistry:
|
|
126
142
|
raise LookupError(f"Model '{package_label}.{model_name}' not registered.")
|
127
143
|
return model
|
128
144
|
|
129
|
-
def clear_cache(self):
|
145
|
+
def clear_cache(self) -> None:
|
130
146
|
"""
|
131
147
|
Clear all internal caches, for methods that alter the app registry.
|
132
148
|
|
@@ -140,9 +156,11 @@ class ModelsRegistry:
|
|
140
156
|
# This particularly prevents that an empty value is cached while cloning.
|
141
157
|
for package_models in self.all_models.values():
|
142
158
|
for model in package_models.values():
|
143
|
-
model.
|
159
|
+
model._model_meta._expire_cache()
|
144
160
|
|
145
|
-
def lazy_model_operation(
|
161
|
+
def lazy_model_operation(
|
162
|
+
self, function: Callable[..., None], *model_keys: tuple[str, str]
|
163
|
+
) -> None:
|
146
164
|
"""
|
147
165
|
Take a function and a number of ("package_label", "modelname") tuples, and
|
148
166
|
when all the corresponding models have been imported and registered,
|
@@ -165,11 +183,11 @@ class ModelsRegistry:
|
|
165
183
|
# This will be executed after the class corresponding to next_model
|
166
184
|
# has been imported and registered. The `func` attribute provides
|
167
185
|
# duck-type compatibility with partials.
|
168
|
-
def apply_next_model(model):
|
169
|
-
next_function = partial(apply_next_model.func, model)
|
186
|
+
def apply_next_model(model: type[Model]) -> None:
|
187
|
+
next_function = partial(apply_next_model.func, model) # type: ignore[attr-defined]
|
170
188
|
self.lazy_model_operation(next_function, *more_models)
|
171
189
|
|
172
|
-
apply_next_model.func = function
|
190
|
+
apply_next_model.func = function # type: ignore[attr-defined]
|
173
191
|
|
174
192
|
# If the model has already been imported and registered, partially
|
175
193
|
# apply it to the function now. If not, add it to the list of
|
@@ -182,12 +200,12 @@ class ModelsRegistry:
|
|
182
200
|
else:
|
183
201
|
apply_next_model(model_class)
|
184
202
|
|
185
|
-
def do_pending_operations(self, model):
|
203
|
+
def do_pending_operations(self, model: type[Model]) -> None:
|
186
204
|
"""
|
187
205
|
Take a newly-prepared model and pass it to each function waiting for
|
188
206
|
it. This is called at the very end of Models.register_model().
|
189
207
|
"""
|
190
|
-
key = model.
|
208
|
+
key = model.model_options.package_label, model.model_options.model_name
|
191
209
|
for function in self._pending_operations.pop(key, []):
|
192
210
|
function(model)
|
193
211
|
|
@@ -196,8 +214,9 @@ models_registry = ModelsRegistry()
|
|
196
214
|
|
197
215
|
|
198
216
|
# Decorator to register a model (using the internal registry for the correct state).
|
199
|
-
def register_model(model_class):
|
200
|
-
model_class.
|
201
|
-
model_class.
|
217
|
+
def register_model(model_class: M) -> M:
|
218
|
+
model_class._model_meta.models_registry.register_model(
|
219
|
+
model_class.model_options.package_label,
|
220
|
+
model_class, # type: ignore[arg-type]
|
202
221
|
)
|
203
222
|
return model_class
|