plain.models 0.49.1__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +23 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +22 -12
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +29 -16
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +267 -165
- plain/models/backends/base/validation.py +12 -3
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +12 -3
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +106 -39
- plain/models/backends/mysql/schema.py +48 -24
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +109 -42
- plain/models/backends/postgresql/schema.py +85 -46
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +125 -42
- plain/models/backends/sqlite3/schema.py +82 -58
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +113 -74
- plain/models/cli.py +94 -63
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +65 -47
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +66 -43
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +440 -257
- plain/models/fields/__init__.py +253 -202
- plain/models/fields/json.py +120 -54
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +284 -252
- plain/models/fields/related_descriptors.py +34 -25
- plain/models/fields/related_lookups.py +23 -11
- plain/models/fields/related_managers.py +81 -47
- plain/models/fields/reverse_related.py +58 -55
- plain/models/forms.py +89 -63
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +52 -28
- plain/models/lookups.py +228 -153
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +37 -19
- plain/models/migrations/operations/fields.py +89 -42
- plain/models/migrations/operations/models.py +245 -143
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +18 -11
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +220 -133
- plain/models/migrations/utils.py +29 -13
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +63 -56
- plain/models/otel.py +16 -6
- plain/models/preflight.py +35 -12
- plain/models/query.py +323 -228
- plain/models/query_utils.py +93 -58
- plain/models/registry.py +34 -16
- plain/models/sql/compiler.py +146 -97
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +255 -169
- plain/models/sql/subqueries.py +32 -21
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +13 -5
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
- plain_models-0.50.0.dist-info/RECORD +122 -0
- plain_models-0.49.1.dist-info/RECORD +0 -122
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
plain/models/query_utils.py
CHANGED
@@ -6,16 +6,24 @@ 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.sql.compiler import SQLCompiler
|
26
|
+
|
19
27
|
logger = logging.getLogger("plain.models")
|
20
28
|
|
21
29
|
# PathInfo is used when converting lookups (fk__somecol). The contents
|
@@ -27,7 +35,7 @@ PathInfo = namedtuple(
|
|
27
35
|
)
|
28
36
|
|
29
37
|
|
30
|
-
def subclasses(cls):
|
38
|
+
def subclasses(cls: type) -> Generator[type, None, None]:
|
31
39
|
yield cls
|
32
40
|
for subclass in cls.__subclasses__():
|
33
41
|
yield from subclasses(subclass)
|
@@ -46,14 +54,20 @@ class Q(tree.Node):
|
|
46
54
|
default = AND
|
47
55
|
conditional = True
|
48
56
|
|
49
|
-
def __init__(
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
*args: Any,
|
60
|
+
_connector: str | None = None,
|
61
|
+
_negated: bool = False,
|
62
|
+
**kwargs: Any,
|
63
|
+
) -> None:
|
50
64
|
super().__init__(
|
51
65
|
children=[*args, *sorted(kwargs.items())],
|
52
66
|
connector=_connector,
|
53
67
|
negated=_negated,
|
54
68
|
)
|
55
69
|
|
56
|
-
def _combine(self, other, conn):
|
70
|
+
def _combine(self, other: Any, conn: str) -> Q:
|
57
71
|
if getattr(other, "conditional", False) is False:
|
58
72
|
raise TypeError(other)
|
59
73
|
if not self:
|
@@ -66,26 +80,31 @@ class Q(tree.Node):
|
|
66
80
|
obj.add(other, conn)
|
67
81
|
return obj
|
68
82
|
|
69
|
-
def __or__(self, other):
|
83
|
+
def __or__(self, other: Any) -> Q:
|
70
84
|
return self._combine(other, self.OR)
|
71
85
|
|
72
|
-
def __and__(self, other):
|
86
|
+
def __and__(self, other: Any) -> Q:
|
73
87
|
return self._combine(other, self.AND)
|
74
88
|
|
75
|
-
def __xor__(self, other):
|
89
|
+
def __xor__(self, other: Any) -> Q:
|
76
90
|
return self._combine(other, self.XOR)
|
77
91
|
|
78
|
-
def __invert__(self):
|
92
|
+
def __invert__(self) -> Q:
|
79
93
|
obj = self.copy()
|
80
94
|
obj.negate()
|
81
95
|
return obj
|
82
96
|
|
83
97
|
def resolve_expression(
|
84
|
-
self,
|
85
|
-
|
98
|
+
self,
|
99
|
+
query: Any = None,
|
100
|
+
allow_joins: bool = True,
|
101
|
+
reuse: Any = None,
|
102
|
+
summarize: bool = False,
|
103
|
+
for_save: bool = False,
|
104
|
+
) -> Any:
|
86
105
|
# We must promote any new joins to left outer joins so that when Q is
|
87
106
|
# used as an expression, rows aren't filtered due to joins.
|
88
|
-
clause, joins = query._add_q(
|
107
|
+
clause, joins = query._add_q( # type: ignore[union-attr]
|
89
108
|
self,
|
90
109
|
reuse,
|
91
110
|
allow_joins=allow_joins,
|
@@ -93,10 +112,10 @@ class Q(tree.Node):
|
|
93
112
|
check_filterable=False,
|
94
113
|
summarize=summarize,
|
95
114
|
)
|
96
|
-
query.promote_joins(joins)
|
115
|
+
query.promote_joins(joins) # type: ignore[union-attr]
|
97
116
|
return clause
|
98
117
|
|
99
|
-
def flatten(self):
|
118
|
+
def flatten(self) -> Generator[Any, None, None]:
|
100
119
|
"""
|
101
120
|
Recursively yield this Q object and all subexpressions, in depth-first
|
102
121
|
order.
|
@@ -111,7 +130,7 @@ class Q(tree.Node):
|
|
111
130
|
else:
|
112
131
|
yield child
|
113
132
|
|
114
|
-
def check(self, against):
|
133
|
+
def check(self, against: dict[str, Any]) -> bool:
|
115
134
|
"""
|
116
135
|
Do a database query to check if the expressions of the Q instance
|
117
136
|
matches against the expressions.
|
@@ -123,7 +142,7 @@ class Q(tree.Node):
|
|
123
142
|
from plain.models.sql import Query
|
124
143
|
from plain.models.sql.constants import SINGLE
|
125
144
|
|
126
|
-
query = Query(None)
|
145
|
+
query = Query(None) # type: ignore[arg-type]
|
127
146
|
for name, value in against.items():
|
128
147
|
if not hasattr(value, "resolve_expression"):
|
129
148
|
value = Value(value)
|
@@ -141,12 +160,12 @@ class Q(tree.Node):
|
|
141
160
|
logger.warning("Got a database error calling check() on %r: %s", self, e)
|
142
161
|
return True
|
143
162
|
|
144
|
-
def deconstruct(self):
|
163
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
145
164
|
path = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
146
165
|
if path.startswith("plain.models.query_utils"):
|
147
166
|
path = path.replace("plain.models.query_utils", "plain.models")
|
148
167
|
args = tuple(self.children)
|
149
|
-
kwargs = {}
|
168
|
+
kwargs: dict[str, Any] = {}
|
150
169
|
if self.connector != self.default:
|
151
170
|
kwargs["_connector"] = self.connector
|
152
171
|
if self.negated:
|
@@ -160,10 +179,10 @@ class DeferredAttribute:
|
|
160
179
|
object the first time, the query is executed.
|
161
180
|
"""
|
162
181
|
|
163
|
-
def __init__(self, field):
|
182
|
+
def __init__(self, field: Any) -> None:
|
164
183
|
self.field = field
|
165
184
|
|
166
|
-
def __get__(self, instance, cls=None):
|
185
|
+
def __get__(self, instance: Any, cls: type | None = None) -> Any:
|
167
186
|
"""
|
168
187
|
Retrieve and caches the value from the datastore on the first lookup.
|
169
188
|
Return the cached value.
|
@@ -183,37 +202,37 @@ class class_or_instance_method:
|
|
183
202
|
the caller type (instance or class of models.Field).
|
184
203
|
"""
|
185
204
|
|
186
|
-
def __init__(self, class_method, instance_method):
|
205
|
+
def __init__(self, class_method: Any, instance_method: Any) -> None:
|
187
206
|
self.class_method = class_method
|
188
207
|
self.instance_method = instance_method
|
189
208
|
|
190
|
-
def __get__(self, instance, owner):
|
209
|
+
def __get__(self, instance: Any, owner: type) -> Any:
|
191
210
|
if instance is None:
|
192
211
|
return functools.partial(self.class_method, owner)
|
193
212
|
return functools.partial(self.instance_method, instance)
|
194
213
|
|
195
214
|
|
196
215
|
class RegisterLookupMixin:
|
197
|
-
def _get_lookup(self, lookup_name):
|
216
|
+
def _get_lookup(self, lookup_name: str) -> type | None:
|
198
217
|
return self.get_lookups().get(lookup_name, None)
|
199
218
|
|
200
219
|
@functools.cache
|
201
|
-
def get_class_lookups(cls):
|
220
|
+
def get_class_lookups(cls: type) -> dict[str, type]:
|
202
221
|
class_lookups = [
|
203
222
|
parent.__dict__.get("class_lookups", {}) for parent in inspect.getmro(cls)
|
204
223
|
]
|
205
|
-
return cls.merge_dicts(class_lookups)
|
224
|
+
return cls.merge_dicts(class_lookups) # type: ignore[attr-defined]
|
206
225
|
|
207
|
-
def get_instance_lookups(self):
|
226
|
+
def get_instance_lookups(self) -> dict[str, type]:
|
208
227
|
class_lookups = self.get_class_lookups()
|
209
228
|
if instance_lookups := getattr(self, "instance_lookups", None):
|
210
229
|
return {**class_lookups, **instance_lookups}
|
211
230
|
return class_lookups
|
212
231
|
|
213
232
|
get_lookups = class_or_instance_method(get_class_lookups, get_instance_lookups)
|
214
|
-
get_class_lookups = classmethod(get_class_lookups)
|
233
|
+
get_class_lookups = classmethod(get_class_lookups) # type: ignore[assignment]
|
215
234
|
|
216
|
-
def get_lookup(self, lookup_name):
|
235
|
+
def get_lookup(self, lookup_name: str) -> type | None:
|
217
236
|
from plain.models.lookups import Lookup
|
218
237
|
|
219
238
|
found = self._get_lookup(lookup_name)
|
@@ -223,7 +242,7 @@ class RegisterLookupMixin:
|
|
223
242
|
return None
|
224
243
|
return found
|
225
244
|
|
226
|
-
def get_transform(self, lookup_name):
|
245
|
+
def get_transform(self, lookup_name: str) -> type | None:
|
227
246
|
from plain.models.lookups import Transform
|
228
247
|
|
229
248
|
found = self._get_lookup(lookup_name)
|
@@ -234,33 +253,37 @@ class RegisterLookupMixin:
|
|
234
253
|
return found
|
235
254
|
|
236
255
|
@staticmethod
|
237
|
-
def merge_dicts(dicts):
|
256
|
+
def merge_dicts(dicts: list[dict[str, type]]) -> dict[str, type]:
|
238
257
|
"""
|
239
258
|
Merge dicts in reverse to preference the order of the original list. e.g.,
|
240
259
|
merge_dicts([a, b]) will preference the keys in 'a' over those in 'b'.
|
241
260
|
"""
|
242
|
-
merged = {}
|
261
|
+
merged: dict[str, type] = {}
|
243
262
|
for d in reversed(dicts):
|
244
263
|
merged.update(d)
|
245
264
|
return merged
|
246
265
|
|
247
266
|
@classmethod
|
248
|
-
def _clear_cached_class_lookups(cls):
|
267
|
+
def _clear_cached_class_lookups(cls) -> None:
|
249
268
|
for subclass in subclasses(cls):
|
250
|
-
subclass.get_class_lookups.cache_clear()
|
269
|
+
subclass.get_class_lookups.cache_clear() # type: ignore[attr-defined]
|
251
270
|
|
252
|
-
def register_class_lookup(
|
271
|
+
def register_class_lookup(
|
272
|
+
cls: type, lookup: type, lookup_name: str | None = None
|
273
|
+
) -> type:
|
253
274
|
if lookup_name is None:
|
254
|
-
lookup_name = lookup.lookup_name
|
275
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
255
276
|
if "class_lookups" not in cls.__dict__:
|
256
|
-
cls.class_lookups = {}
|
257
|
-
cls.class_lookups[lookup_name] = lookup
|
258
|
-
cls._clear_cached_class_lookups()
|
277
|
+
cls.class_lookups = {} # type: ignore[attr-defined]
|
278
|
+
cls.class_lookups[lookup_name] = lookup # type: ignore[attr-defined]
|
279
|
+
cls._clear_cached_class_lookups() # type: ignore[attr-defined]
|
259
280
|
return lookup
|
260
281
|
|
261
|
-
def register_instance_lookup(
|
282
|
+
def register_instance_lookup(
|
283
|
+
self, lookup: type, lookup_name: str | None = None
|
284
|
+
) -> type:
|
262
285
|
if lookup_name is None:
|
263
|
-
lookup_name = lookup.lookup_name
|
286
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
264
287
|
if "instance_lookups" not in self.__dict__:
|
265
288
|
self.instance_lookups = {}
|
266
289
|
self.instance_lookups[lookup_name] = lookup
|
@@ -269,34 +292,44 @@ class RegisterLookupMixin:
|
|
269
292
|
register_lookup = class_or_instance_method(
|
270
293
|
register_class_lookup, register_instance_lookup
|
271
294
|
)
|
272
|
-
register_class_lookup = classmethod(register_class_lookup)
|
295
|
+
register_class_lookup = classmethod(register_class_lookup) # type: ignore[assignment]
|
273
296
|
|
274
|
-
def _unregister_class_lookup(
|
297
|
+
def _unregister_class_lookup(
|
298
|
+
cls: type, lookup: type, lookup_name: str | None = None
|
299
|
+
) -> None:
|
275
300
|
"""
|
276
301
|
Remove given lookup from cls lookups. For use in tests only as it's
|
277
302
|
not thread-safe.
|
278
303
|
"""
|
279
304
|
if lookup_name is None:
|
280
|
-
lookup_name = lookup.lookup_name
|
281
|
-
del cls.class_lookups[lookup_name]
|
282
|
-
cls._clear_cached_class_lookups()
|
305
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
306
|
+
del cls.class_lookups[lookup_name] # type: ignore[attr-defined]
|
307
|
+
cls._clear_cached_class_lookups() # type: ignore[attr-defined]
|
283
308
|
|
284
|
-
def _unregister_instance_lookup(
|
309
|
+
def _unregister_instance_lookup(
|
310
|
+
self, lookup: type, lookup_name: str | None = None
|
311
|
+
) -> None:
|
285
312
|
"""
|
286
313
|
Remove given lookup from instance lookups. For use in tests only as
|
287
314
|
it's not thread-safe.
|
288
315
|
"""
|
289
316
|
if lookup_name is None:
|
290
|
-
lookup_name = lookup.lookup_name
|
317
|
+
lookup_name = lookup.lookup_name # type: ignore[attr-defined]
|
291
318
|
del self.instance_lookups[lookup_name]
|
292
319
|
|
293
320
|
_unregister_lookup = class_or_instance_method(
|
294
321
|
_unregister_class_lookup, _unregister_instance_lookup
|
295
322
|
)
|
296
|
-
_unregister_class_lookup = classmethod(_unregister_class_lookup)
|
323
|
+
_unregister_class_lookup = classmethod(_unregister_class_lookup) # type: ignore[assignment]
|
297
324
|
|
298
325
|
|
299
|
-
def select_related_descend(
|
326
|
+
def select_related_descend(
|
327
|
+
field: Any,
|
328
|
+
restricted: bool,
|
329
|
+
requested: dict[str, Any],
|
330
|
+
select_mask: Any,
|
331
|
+
reverse: bool = False,
|
332
|
+
) -> bool:
|
300
333
|
"""
|
301
334
|
Return True if this field should be used to descend deeper for
|
302
335
|
select_related() purposes. Used by both the query construction code
|
@@ -333,7 +366,9 @@ def select_related_descend(field, restricted, requested, select_mask, reverse=Fa
|
|
333
366
|
return True
|
334
367
|
|
335
368
|
|
336
|
-
def refs_expression(
|
369
|
+
def refs_expression(
|
370
|
+
lookup_parts: list[str], annotations: dict[str, Any]
|
371
|
+
) -> tuple[str | None, tuple[str, ...]]:
|
337
372
|
"""
|
338
373
|
Check if the lookup_parts contains references to the given annotations set.
|
339
374
|
Because the LOOKUP_SEP is contained in the default annotation names, check
|
@@ -342,11 +377,11 @@ def refs_expression(lookup_parts, annotations):
|
|
342
377
|
for n in range(1, len(lookup_parts) + 1):
|
343
378
|
level_n_lookup = LOOKUP_SEP.join(lookup_parts[0:n])
|
344
379
|
if annotations.get(level_n_lookup):
|
345
|
-
return level_n_lookup, lookup_parts[n:]
|
380
|
+
return level_n_lookup, tuple(lookup_parts[n:])
|
346
381
|
return None, ()
|
347
382
|
|
348
383
|
|
349
|
-
def check_rel_lookup_compatibility(model, target_opts, field):
|
384
|
+
def check_rel_lookup_compatibility(model: type, target_opts: Any, field: Any) -> bool:
|
350
385
|
"""
|
351
386
|
Check that self.model is compatible with target_opts. Compatibility
|
352
387
|
is OK if:
|
@@ -354,7 +389,7 @@ def check_rel_lookup_compatibility(model, target_opts, field):
|
|
354
389
|
2) model is parent of opts' model or the other way around
|
355
390
|
"""
|
356
391
|
|
357
|
-
def check(opts):
|
392
|
+
def check(opts: Any) -> bool:
|
358
393
|
return model == opts.model
|
359
394
|
|
360
395
|
# If the field is a primary key, then doing a query against the field's
|
@@ -374,17 +409,17 @@ def check_rel_lookup_compatibility(model, target_opts, field):
|
|
374
409
|
class FilteredRelation:
|
375
410
|
"""Specify custom filtering in the ON clause of SQL joins."""
|
376
411
|
|
377
|
-
def __init__(self, relation_name, *, condition=Q()):
|
412
|
+
def __init__(self, relation_name: str, *, condition: Q = Q()) -> None:
|
378
413
|
if not relation_name:
|
379
414
|
raise ValueError("relation_name cannot be empty.")
|
380
415
|
self.relation_name = relation_name
|
381
|
-
self.alias = None
|
416
|
+
self.alias: str | None = None
|
382
417
|
if not isinstance(condition, Q):
|
383
418
|
raise ValueError("condition argument must be a Q() instance.")
|
384
419
|
self.condition = condition
|
385
|
-
self.path = []
|
420
|
+
self.path: list[str] = []
|
386
421
|
|
387
|
-
def __eq__(self, other):
|
422
|
+
def __eq__(self, other: object) -> bool:
|
388
423
|
if not isinstance(other, self.__class__):
|
389
424
|
return NotImplemented
|
390
425
|
return (
|
@@ -393,20 +428,20 @@ class FilteredRelation:
|
|
393
428
|
and self.condition == other.condition
|
394
429
|
)
|
395
430
|
|
396
|
-
def clone(self):
|
431
|
+
def clone(self) -> FilteredRelation:
|
397
432
|
clone = FilteredRelation(self.relation_name, condition=self.condition)
|
398
433
|
clone.alias = self.alias
|
399
434
|
clone.path = self.path[:]
|
400
435
|
return clone
|
401
436
|
|
402
|
-
def resolve_expression(self, *args, **kwargs):
|
437
|
+
def resolve_expression(self, *args: Any, **kwargs: Any) -> Any:
|
403
438
|
"""
|
404
439
|
QuerySet.annotate() only accepts expression-like arguments
|
405
440
|
(with a resolve_expression() method).
|
406
441
|
"""
|
407
442
|
raise NotImplementedError("FilteredRelation.resolve_expression() is unused.")
|
408
443
|
|
409
|
-
def as_sql(self, compiler, connection):
|
444
|
+
def as_sql(self, compiler: SQLCompiler, connection: BaseDatabaseWrapper) -> Any:
|
410
445
|
# Resolve the condition in Join.filtered_relation.
|
411
446
|
query = compiler.query
|
412
447
|
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,7 +103,7 @@ 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().
|
@@ -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
|
|
@@ -142,7 +158,9 @@ class ModelsRegistry:
|
|
142
158
|
for model in package_models.values():
|
143
159
|
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,7 +200,7 @@ 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().
|
@@ -196,7 +214,7 @@ 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):
|
217
|
+
def register_model(model_class: M) -> M:
|
200
218
|
model_class._meta.models_registry.register_model(
|
201
219
|
model_class._meta.package_label, model_class
|
202
220
|
)
|