plain.models 0.49.2__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/models/CHANGELOG.md +13 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +22 -12
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +29 -16
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +267 -165
- plain/models/backends/base/validation.py +12 -3
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +12 -3
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +106 -39
- plain/models/backends/mysql/schema.py +48 -24
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +109 -42
- plain/models/backends/postgresql/schema.py +85 -46
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +125 -42
- plain/models/backends/sqlite3/schema.py +82 -58
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +113 -74
- plain/models/cli.py +94 -63
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +65 -47
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +66 -43
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +440 -257
- plain/models/fields/__init__.py +253 -202
- plain/models/fields/json.py +120 -54
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +284 -252
- plain/models/fields/related_descriptors.py +31 -22
- plain/models/fields/related_lookups.py +23 -11
- plain/models/fields/related_managers.py +81 -47
- plain/models/fields/reverse_related.py +58 -55
- plain/models/forms.py +89 -63
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +52 -28
- plain/models/lookups.py +228 -153
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +37 -19
- plain/models/migrations/operations/fields.py +89 -42
- plain/models/migrations/operations/models.py +245 -143
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +18 -11
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +220 -133
- plain/models/migrations/utils.py +29 -13
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +63 -56
- plain/models/otel.py +16 -6
- plain/models/preflight.py +35 -12
- plain/models/query.py +323 -228
- plain/models/query_utils.py +93 -58
- plain/models/registry.py +34 -16
- plain/models/sql/compiler.py +146 -97
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +255 -169
- plain/models/sql/subqueries.py +32 -21
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +13 -5
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
- plain_models-0.50.0.dist-info/RECORD +122 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
plain/models/constraints.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from enum import Enum
|
2
4
|
from types import NoneType
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.exceptions import ValidationError
|
5
8
|
from plain.models.exceptions import FieldError
|
@@ -13,13 +16,17 @@ __all__ = ["BaseConstraint", "CheckConstraint", "Deferrable", "UniqueConstraint"
|
|
13
16
|
|
14
17
|
|
15
18
|
class BaseConstraint:
|
16
|
-
default_violation_error_message =
|
17
|
-
violation_error_code = None
|
18
|
-
violation_error_message = None
|
19
|
+
default_violation_error_message = 'Constraint "%(name)s" is violated.'
|
20
|
+
violation_error_code: str | None = None
|
21
|
+
violation_error_message: str | None = None
|
19
22
|
|
20
23
|
def __init__(
|
21
|
-
self,
|
22
|
-
|
24
|
+
self,
|
25
|
+
*,
|
26
|
+
name: str,
|
27
|
+
violation_error_code: str | None = None,
|
28
|
+
violation_error_message: str | None = None,
|
29
|
+
) -> None:
|
23
30
|
self.name = name
|
24
31
|
if violation_error_code is not None:
|
25
32
|
self.violation_error_code = violation_error_code
|
@@ -29,28 +36,30 @@ class BaseConstraint:
|
|
29
36
|
self.violation_error_message = self.default_violation_error_message
|
30
37
|
|
31
38
|
@property
|
32
|
-
def contains_expressions(self):
|
39
|
+
def contains_expressions(self) -> bool:
|
33
40
|
return False
|
34
41
|
|
35
|
-
def constraint_sql(self, model, schema_editor):
|
42
|
+
def constraint_sql(self, model: Any, schema_editor: Any) -> str:
|
36
43
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
37
44
|
|
38
|
-
def create_sql(self, model, schema_editor):
|
45
|
+
def create_sql(self, model: Any, schema_editor: Any) -> str:
|
39
46
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
40
47
|
|
41
|
-
def remove_sql(self, model, schema_editor):
|
48
|
+
def remove_sql(self, model: Any, schema_editor: Any) -> str:
|
42
49
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
43
50
|
|
44
|
-
def validate(
|
51
|
+
def validate(
|
52
|
+
self, model: Any, instance: Any, exclude: set[str] | None = None
|
53
|
+
) -> None:
|
45
54
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
46
55
|
|
47
|
-
def get_violation_error_message(self):
|
48
|
-
return self.violation_error_message % {"name": self.name}
|
56
|
+
def get_violation_error_message(self) -> str:
|
57
|
+
return self.violation_error_message % {"name": self.name} # type: ignore[operator]
|
49
58
|
|
50
|
-
def deconstruct(self):
|
59
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
51
60
|
path = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
52
61
|
path = path.replace("plain.models.constraints", "plain.models")
|
53
|
-
kwargs = {"name": self.name}
|
62
|
+
kwargs: dict[str, Any] = {"name": self.name}
|
54
63
|
if (
|
55
64
|
self.violation_error_message is not None
|
56
65
|
and self.violation_error_message != self.default_violation_error_message
|
@@ -60,15 +69,20 @@ class BaseConstraint:
|
|
60
69
|
kwargs["violation_error_code"] = self.violation_error_code
|
61
70
|
return (path, (), kwargs)
|
62
71
|
|
63
|
-
def clone(self):
|
72
|
+
def clone(self) -> BaseConstraint:
|
64
73
|
_, args, kwargs = self.deconstruct()
|
65
74
|
return self.__class__(*args, **kwargs)
|
66
75
|
|
67
76
|
|
68
77
|
class CheckConstraint(BaseConstraint):
|
69
78
|
def __init__(
|
70
|
-
self,
|
71
|
-
|
79
|
+
self,
|
80
|
+
*,
|
81
|
+
check: Q,
|
82
|
+
name: str,
|
83
|
+
violation_error_code: str | None = None,
|
84
|
+
violation_error_message: str | None = None,
|
85
|
+
) -> None:
|
72
86
|
self.check = check
|
73
87
|
if not getattr(check, "conditional", False):
|
74
88
|
raise TypeError(
|
@@ -80,25 +94,27 @@ class CheckConstraint(BaseConstraint):
|
|
80
94
|
violation_error_message=violation_error_message,
|
81
95
|
)
|
82
96
|
|
83
|
-
def _get_check_sql(self, model, schema_editor):
|
97
|
+
def _get_check_sql(self, model: Any, schema_editor: Any) -> str:
|
84
98
|
query = Query(model=model, alias_cols=False)
|
85
99
|
where = query.build_where(self.check)
|
86
100
|
compiler = query.get_compiler()
|
87
101
|
sql, params = where.as_sql(compiler, schema_editor.connection)
|
88
102
|
return sql % tuple(schema_editor.quote_value(p) for p in params)
|
89
103
|
|
90
|
-
def constraint_sql(self, model, schema_editor):
|
104
|
+
def constraint_sql(self, model: Any, schema_editor: Any) -> str:
|
91
105
|
check = self._get_check_sql(model, schema_editor)
|
92
106
|
return schema_editor._check_sql(self.name, check)
|
93
107
|
|
94
|
-
def create_sql(self, model, schema_editor):
|
108
|
+
def create_sql(self, model: Any, schema_editor: Any) -> str:
|
95
109
|
check = self._get_check_sql(model, schema_editor)
|
96
110
|
return schema_editor._create_check_sql(model, self.name, check)
|
97
111
|
|
98
|
-
def remove_sql(self, model, schema_editor):
|
112
|
+
def remove_sql(self, model: Any, schema_editor: Any) -> str:
|
99
113
|
return schema_editor._delete_check_sql(model, self.name)
|
100
114
|
|
101
|
-
def validate(
|
115
|
+
def validate(
|
116
|
+
self, model: Any, instance: Any, exclude: set[str] | None = None
|
117
|
+
) -> None:
|
102
118
|
against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
|
103
119
|
try:
|
104
120
|
if not Q(self.check).check(against):
|
@@ -108,7 +124,7 @@ class CheckConstraint(BaseConstraint):
|
|
108
124
|
except FieldError:
|
109
125
|
pass
|
110
126
|
|
111
|
-
def __repr__(self):
|
127
|
+
def __repr__(self) -> str:
|
112
128
|
return "<{}: check={} name={}{}{}>".format(
|
113
129
|
self.__class__.__qualname__,
|
114
130
|
self.check,
|
@@ -126,7 +142,7 @@ class CheckConstraint(BaseConstraint):
|
|
126
142
|
),
|
127
143
|
)
|
128
144
|
|
129
|
-
def __eq__(self, other):
|
145
|
+
def __eq__(self, other: object) -> bool:
|
130
146
|
if isinstance(other, CheckConstraint):
|
131
147
|
return (
|
132
148
|
self.name == other.name
|
@@ -136,7 +152,7 @@ class CheckConstraint(BaseConstraint):
|
|
136
152
|
)
|
137
153
|
return super().__eq__(other)
|
138
154
|
|
139
|
-
def deconstruct(self):
|
155
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
140
156
|
path, args, kwargs = super().deconstruct()
|
141
157
|
kwargs["check"] = self.check
|
142
158
|
return path, args, kwargs
|
@@ -147,23 +163,23 @@ class Deferrable(Enum):
|
|
147
163
|
IMMEDIATE = "immediate"
|
148
164
|
|
149
165
|
# A similar format was proposed for Python 3.10.
|
150
|
-
def __repr__(self):
|
166
|
+
def __repr__(self) -> str:
|
151
167
|
return f"{self.__class__.__qualname__}.{self._name_}"
|
152
168
|
|
153
169
|
|
154
170
|
class UniqueConstraint(BaseConstraint):
|
155
171
|
def __init__(
|
156
172
|
self,
|
157
|
-
*expressions,
|
158
|
-
fields=(),
|
159
|
-
name=None,
|
160
|
-
condition=None,
|
161
|
-
deferrable=None,
|
162
|
-
include=None,
|
163
|
-
opclasses=(),
|
164
|
-
violation_error_code=None,
|
165
|
-
violation_error_message=None,
|
166
|
-
):
|
173
|
+
*expressions: Any,
|
174
|
+
fields: tuple[str, ...] | list[str] = (),
|
175
|
+
name: str | None = None,
|
176
|
+
condition: Q | None = None,
|
177
|
+
deferrable: Deferrable | None = None,
|
178
|
+
include: tuple[str, ...] | list[str] | None = None,
|
179
|
+
opclasses: tuple[str, ...] | list[str] = (),
|
180
|
+
violation_error_code: str | None = None,
|
181
|
+
violation_error_message: str | None = None,
|
182
|
+
) -> None:
|
167
183
|
if not name:
|
168
184
|
raise ValueError("A unique constraint must be named.")
|
169
185
|
if not expressions and not fields:
|
@@ -213,16 +229,16 @@ class UniqueConstraint(BaseConstraint):
|
|
213
229
|
for expression in expressions
|
214
230
|
)
|
215
231
|
super().__init__(
|
216
|
-
name=name,
|
232
|
+
name=name, # type: ignore[arg-type]
|
217
233
|
violation_error_code=violation_error_code,
|
218
234
|
violation_error_message=violation_error_message,
|
219
235
|
)
|
220
236
|
|
221
237
|
@property
|
222
|
-
def contains_expressions(self):
|
238
|
+
def contains_expressions(self) -> bool:
|
223
239
|
return bool(self.expressions)
|
224
240
|
|
225
|
-
def _get_condition_sql(self, model, schema_editor):
|
241
|
+
def _get_condition_sql(self, model: Any, schema_editor: Any) -> str | None:
|
226
242
|
if self.condition is None:
|
227
243
|
return None
|
228
244
|
query = Query(model=model, alias_cols=False)
|
@@ -231,7 +247,7 @@ class UniqueConstraint(BaseConstraint):
|
|
231
247
|
sql, params = where.as_sql(compiler, schema_editor.connection)
|
232
248
|
return sql % tuple(schema_editor.quote_value(p) for p in params)
|
233
249
|
|
234
|
-
def _get_index_expressions(self, model, schema_editor):
|
250
|
+
def _get_index_expressions(self, model: Any, schema_editor: Any) -> Any:
|
235
251
|
if not self.expressions:
|
236
252
|
return None
|
237
253
|
index_expressions = []
|
@@ -243,7 +259,7 @@ class UniqueConstraint(BaseConstraint):
|
|
243
259
|
Query(model, alias_cols=False),
|
244
260
|
)
|
245
261
|
|
246
|
-
def constraint_sql(self, model, schema_editor):
|
262
|
+
def constraint_sql(self, model: Any, schema_editor: Any) -> str:
|
247
263
|
fields = [model._meta.get_field(field_name) for field_name in self.fields]
|
248
264
|
include = [
|
249
265
|
model._meta.get_field(field_name).column for field_name in self.include
|
@@ -261,7 +277,7 @@ class UniqueConstraint(BaseConstraint):
|
|
261
277
|
expressions=expressions,
|
262
278
|
)
|
263
279
|
|
264
|
-
def create_sql(self, model, schema_editor):
|
280
|
+
def create_sql(self, model: Any, schema_editor: Any) -> str:
|
265
281
|
fields = [model._meta.get_field(field_name) for field_name in self.fields]
|
266
282
|
include = [
|
267
283
|
model._meta.get_field(field_name).column for field_name in self.include
|
@@ -279,7 +295,7 @@ class UniqueConstraint(BaseConstraint):
|
|
279
295
|
expressions=expressions,
|
280
296
|
)
|
281
297
|
|
282
|
-
def remove_sql(self, model, schema_editor):
|
298
|
+
def remove_sql(self, model: Any, schema_editor: Any) -> str:
|
283
299
|
condition = self._get_condition_sql(model, schema_editor)
|
284
300
|
include = [
|
285
301
|
model._meta.get_field(field_name).column for field_name in self.include
|
@@ -295,7 +311,7 @@ class UniqueConstraint(BaseConstraint):
|
|
295
311
|
expressions=expressions,
|
296
312
|
)
|
297
313
|
|
298
|
-
def __repr__(self):
|
314
|
+
def __repr__(self) -> str:
|
299
315
|
return "<{}:{}{}{}{}{}{}{}{}{}>".format(
|
300
316
|
self.__class__.__qualname__,
|
301
317
|
"" if not self.fields else f" fields={repr(self.fields)}",
|
@@ -318,7 +334,7 @@ class UniqueConstraint(BaseConstraint):
|
|
318
334
|
),
|
319
335
|
)
|
320
336
|
|
321
|
-
def __eq__(self, other):
|
337
|
+
def __eq__(self, other: object) -> bool:
|
322
338
|
if isinstance(other, UniqueConstraint):
|
323
339
|
return (
|
324
340
|
self.name == other.name
|
@@ -333,7 +349,7 @@ class UniqueConstraint(BaseConstraint):
|
|
333
349
|
)
|
334
350
|
return super().__eq__(other)
|
335
351
|
|
336
|
-
def deconstruct(self):
|
352
|
+
def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
|
337
353
|
path, args, kwargs = super().deconstruct()
|
338
354
|
if self.fields:
|
339
355
|
kwargs["fields"] = self.fields
|
@@ -347,7 +363,9 @@ class UniqueConstraint(BaseConstraint):
|
|
347
363
|
kwargs["opclasses"] = self.opclasses
|
348
364
|
return path, self.expressions, kwargs
|
349
365
|
|
350
|
-
def validate(
|
366
|
+
def validate(
|
367
|
+
self, model: Any, instance: Any, exclude: set[str] | None = None
|
368
|
+
) -> None:
|
351
369
|
queryset = model.query
|
352
370
|
if self.fields:
|
353
371
|
lookup_kwargs = {}
|
plain/models/database_url.py
CHANGED
plain/models/db.py
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
1
5
|
from plain import signals
|
2
6
|
|
3
7
|
from .connections import DatabaseConnection
|
@@ -22,7 +26,7 @@ db_connection = DatabaseConnection()
|
|
22
26
|
|
23
27
|
|
24
28
|
# Register an event to reset saved queries when a Plain request is started.
|
25
|
-
def reset_queries(**kwargs):
|
29
|
+
def reset_queries(**kwargs: Any) -> None:
|
26
30
|
if db_connection.has_connection():
|
27
31
|
db_connection.queries_log.clear()
|
28
32
|
|
@@ -32,7 +36,7 @@ signals.request_started.connect(reset_queries)
|
|
32
36
|
|
33
37
|
# Register an event to reset transaction state and close connections past
|
34
38
|
# their lifetime.
|
35
|
-
def close_old_connections(**kwargs):
|
39
|
+
def close_old_connections(**kwargs: Any) -> None:
|
36
40
|
if db_connection.has_connection():
|
37
41
|
db_connection.close_if_unusable_or_obsolete()
|
38
42
|
|
plain/models/deletion.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
from collections import Counter, defaultdict
|
4
|
+
from collections.abc import Callable, Generator, Iterable
|
2
5
|
from functools import partial, reduce
|
3
6
|
from itertools import chain
|
4
7
|
from operator import attrgetter, or_
|
8
|
+
from typing import TYPE_CHECKING, Any
|
5
9
|
|
6
10
|
from plain.models import (
|
7
11
|
query_utils,
|
@@ -11,23 +15,26 @@ from plain.models import (
|
|
11
15
|
from plain.models.db import IntegrityError, db_connection
|
12
16
|
from plain.models.query import QuerySet
|
13
17
|
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from plain.models.fields import Field
|
20
|
+
|
14
21
|
|
15
22
|
class ProtectedError(IntegrityError):
|
16
|
-
def __init__(self, msg, protected_objects):
|
23
|
+
def __init__(self, msg: str, protected_objects: Iterable[Any]) -> None:
|
17
24
|
self.protected_objects = protected_objects
|
18
25
|
super().__init__(msg, protected_objects)
|
19
26
|
|
20
27
|
|
21
28
|
class RestrictedError(IntegrityError):
|
22
|
-
def __init__(self, msg, restricted_objects):
|
29
|
+
def __init__(self, msg: str, restricted_objects: Iterable[Any]) -> None:
|
23
30
|
self.restricted_objects = restricted_objects
|
24
31
|
super().__init__(msg, restricted_objects)
|
25
32
|
|
26
33
|
|
27
|
-
def CASCADE(collector, field, sub_objs):
|
34
|
+
def CASCADE(collector: Collector, field: Field, sub_objs: Any) -> None:
|
28
35
|
collector.collect(
|
29
36
|
sub_objs,
|
30
|
-
source=field.remote_field.model,
|
37
|
+
source=field.remote_field.model, # type: ignore[attr-defined]
|
31
38
|
nullable=field.allow_null,
|
32
39
|
fail_on_restricted=False,
|
33
40
|
)
|
@@ -35,54 +42,54 @@ def CASCADE(collector, field, sub_objs):
|
|
35
42
|
collector.add_field_update(field, None, sub_objs)
|
36
43
|
|
37
44
|
|
38
|
-
def PROTECT(collector, field, sub_objs):
|
45
|
+
def PROTECT(collector: Collector, field: Field, sub_objs: Any) -> None:
|
39
46
|
raise ProtectedError(
|
40
|
-
f"Cannot delete some instances of model '{field.remote_field.model.__name__}' because they are "
|
47
|
+
f"Cannot delete some instances of model '{field.remote_field.model.__name__}' because they are " # type: ignore[attr-defined]
|
41
48
|
f"referenced through a protected foreign key: '{sub_objs[0].__class__.__name__}.{field.name}'",
|
42
49
|
sub_objs,
|
43
50
|
)
|
44
51
|
|
45
52
|
|
46
|
-
def RESTRICT(collector, field, sub_objs):
|
53
|
+
def RESTRICT(collector: Collector, field: Field, sub_objs: Any) -> None:
|
47
54
|
collector.add_restricted_objects(field, sub_objs)
|
48
|
-
collector.add_dependency(field.remote_field.model, field.model)
|
55
|
+
collector.add_dependency(field.remote_field.model, field.model) # type: ignore[attr-defined]
|
49
56
|
|
50
57
|
|
51
|
-
def SET(value):
|
58
|
+
def SET(value: Any) -> Callable[[Collector, Field, Any], None]:
|
52
59
|
if callable(value):
|
53
60
|
|
54
|
-
def set_on_delete(collector, field, sub_objs):
|
61
|
+
def set_on_delete(collector: Collector, field: Field, sub_objs: Any) -> None:
|
55
62
|
collector.add_field_update(field, value(), sub_objs)
|
56
63
|
|
57
64
|
else:
|
58
65
|
|
59
|
-
def set_on_delete(collector, field, sub_objs):
|
66
|
+
def set_on_delete(collector: Collector, field: Field, sub_objs: Any) -> None:
|
60
67
|
collector.add_field_update(field, value, sub_objs)
|
61
68
|
|
62
|
-
set_on_delete.deconstruct = lambda: ("plain.models.SET", (value,), {})
|
63
|
-
set_on_delete.lazy_sub_objs = True
|
69
|
+
set_on_delete.deconstruct = lambda: ("plain.models.SET", (value,), {}) # type: ignore[attr-defined]
|
70
|
+
set_on_delete.lazy_sub_objs = True # type: ignore[attr-defined]
|
64
71
|
return set_on_delete
|
65
72
|
|
66
73
|
|
67
|
-
def SET_NULL(collector, field, sub_objs):
|
74
|
+
def SET_NULL(collector: Collector, field: Field, sub_objs: Any) -> None:
|
68
75
|
collector.add_field_update(field, None, sub_objs)
|
69
76
|
|
70
77
|
|
71
|
-
SET_NULL.lazy_sub_objs = True
|
78
|
+
SET_NULL.lazy_sub_objs = True # type: ignore[attr-defined]
|
72
79
|
|
73
80
|
|
74
|
-
def SET_DEFAULT(collector, field, sub_objs):
|
81
|
+
def SET_DEFAULT(collector: Collector, field: Field, sub_objs: Any) -> None:
|
75
82
|
collector.add_field_update(field, field.get_default(), sub_objs)
|
76
83
|
|
77
84
|
|
78
|
-
SET_DEFAULT.lazy_sub_objs = True
|
85
|
+
SET_DEFAULT.lazy_sub_objs = True # type: ignore[attr-defined]
|
79
86
|
|
80
87
|
|
81
|
-
def DO_NOTHING(collector, field, sub_objs):
|
88
|
+
def DO_NOTHING(collector: Collector, field: Field, sub_objs: Any) -> None:
|
82
89
|
pass
|
83
90
|
|
84
91
|
|
85
|
-
def get_candidate_relations_to_delete(opts):
|
92
|
+
def get_candidate_relations_to_delete(opts: Any) -> Generator[Any, None, None]:
|
86
93
|
# The candidate relations are the ones that come from N-1 and 1-1 relations.
|
87
94
|
# N-N (i.e., many-to-many) relations aren't candidates for deletion.
|
88
95
|
return (
|
@@ -93,26 +100,38 @@ def get_candidate_relations_to_delete(opts):
|
|
93
100
|
|
94
101
|
|
95
102
|
class Collector:
|
96
|
-
def __init__(self, origin=None):
|
103
|
+
def __init__(self, origin: Any = None) -> None:
|
97
104
|
# A Model or QuerySet object.
|
98
105
|
self.origin = origin
|
99
106
|
# Initially, {model: {instances}}, later values become lists.
|
100
|
-
self.data = defaultdict(set)
|
107
|
+
self.data: defaultdict[Any, Any] = defaultdict(set)
|
101
108
|
# {(field, value): [instances, …]}
|
102
|
-
self.field_updates = defaultdict(
|
109
|
+
self.field_updates: defaultdict[tuple[Field, Any], list[Any]] = defaultdict(
|
110
|
+
list
|
111
|
+
)
|
103
112
|
# {model: {field: {instances}}}
|
104
|
-
self.restricted_objects = defaultdict(
|
113
|
+
self.restricted_objects: defaultdict[Any, Any] = defaultdict(
|
114
|
+
partial(defaultdict, set)
|
115
|
+
)
|
105
116
|
# fast_deletes is a list of queryset-likes that can be deleted without
|
106
117
|
# fetching the objects into memory.
|
107
|
-
self.fast_deletes = []
|
118
|
+
self.fast_deletes: list[Any] = []
|
108
119
|
|
109
120
|
# Tracks deletion-order dependency for databases without transactions
|
110
121
|
# or ability to defer constraint checks. Only concrete model classes
|
111
122
|
# should be included, as the dependencies exist only between actual
|
112
123
|
# database tables.
|
113
|
-
self.dependencies = defaultdict(
|
124
|
+
self.dependencies: defaultdict[Any, set[Any]] = defaultdict(
|
125
|
+
set
|
126
|
+
) # {model: {models}}
|
114
127
|
|
115
|
-
def add(
|
128
|
+
def add(
|
129
|
+
self,
|
130
|
+
objs: Iterable[Any],
|
131
|
+
source: Any = None,
|
132
|
+
nullable: bool = False,
|
133
|
+
reverse_dependency: bool = False,
|
134
|
+
) -> list[Any]:
|
116
135
|
"""
|
117
136
|
Add 'objs' to the collection of objects to be deleted. If the call is
|
118
137
|
the result of a cascade, 'source' should be the model that caused it,
|
@@ -136,32 +155,34 @@ class Collector:
|
|
136
155
|
self.add_dependency(source, model, reverse_dependency=reverse_dependency)
|
137
156
|
return new_objs
|
138
157
|
|
139
|
-
def add_dependency(
|
158
|
+
def add_dependency(
|
159
|
+
self, model: Any, dependency: Any, reverse_dependency: bool = False
|
160
|
+
) -> None:
|
140
161
|
if reverse_dependency:
|
141
162
|
model, dependency = dependency, model
|
142
163
|
self.dependencies[model].add(dependency)
|
143
164
|
self.data.setdefault(dependency, self.data.default_factory())
|
144
165
|
|
145
|
-
def add_field_update(self, field, value, objs):
|
166
|
+
def add_field_update(self, field: Field, value: Any, objs: Iterable[Any]) -> None:
|
146
167
|
"""
|
147
168
|
Schedule a field update. 'objs' must be a homogeneous iterable
|
148
169
|
collection of model instances (e.g. a QuerySet).
|
149
170
|
"""
|
150
171
|
self.field_updates[field, value].append(objs)
|
151
172
|
|
152
|
-
def add_restricted_objects(self, field, objs):
|
173
|
+
def add_restricted_objects(self, field: Field, objs: Iterable[Any]) -> None:
|
153
174
|
if objs:
|
154
175
|
model = objs[0].__class__
|
155
176
|
self.restricted_objects[model][field].update(objs)
|
156
177
|
|
157
|
-
def clear_restricted_objects_from_set(self, model, objs):
|
178
|
+
def clear_restricted_objects_from_set(self, model: Any, objs: set[Any]) -> None:
|
158
179
|
if model in self.restricted_objects:
|
159
180
|
self.restricted_objects[model] = {
|
160
181
|
field: items - objs
|
161
182
|
for field, items in self.restricted_objects[model].items()
|
162
183
|
}
|
163
184
|
|
164
|
-
def clear_restricted_objects_from_queryset(self, model, qs):
|
185
|
+
def clear_restricted_objects_from_queryset(self, model: Any, qs: QuerySet) -> None:
|
165
186
|
if model in self.restricted_objects:
|
166
187
|
objs = set(
|
167
188
|
qs.filter(
|
@@ -174,7 +195,7 @@ class Collector:
|
|
174
195
|
)
|
175
196
|
self.clear_restricted_objects_from_set(model, objs)
|
176
197
|
|
177
|
-
def can_fast_delete(self, objs, from_field=None):
|
198
|
+
def can_fast_delete(self, objs: Any, from_field: Any = None) -> bool:
|
178
199
|
"""
|
179
200
|
Determine if the objects in the given queryset-like or single object
|
180
201
|
can be fast-deleted. This can be done if there are no cascades, no
|
@@ -205,7 +226,7 @@ class Collector:
|
|
205
226
|
)
|
206
227
|
)
|
207
228
|
|
208
|
-
def get_del_batches(self, objs, fields):
|
229
|
+
def get_del_batches(self, objs: list[Any], fields: list[Field]) -> list[list[Any]]:
|
209
230
|
"""
|
210
231
|
Return the objs in suitably sized batches for the used db_connection.
|
211
232
|
"""
|
@@ -224,13 +245,13 @@ class Collector:
|
|
224
245
|
|
225
246
|
def collect(
|
226
247
|
self,
|
227
|
-
objs,
|
228
|
-
source=None,
|
229
|
-
nullable=False,
|
230
|
-
collect_related=True,
|
231
|
-
reverse_dependency=False,
|
232
|
-
fail_on_restricted=True,
|
233
|
-
):
|
248
|
+
objs: Iterable[Any],
|
249
|
+
source: Any = None,
|
250
|
+
nullable: bool = False,
|
251
|
+
collect_related: bool = True,
|
252
|
+
reverse_dependency: bool = False,
|
253
|
+
fail_on_restricted: bool = True,
|
254
|
+
) -> None:
|
234
255
|
"""
|
235
256
|
Add 'objs' to the collection of objects to be deleted as well as all
|
236
257
|
parent instances. 'objs' must be a homogeneous iterable collection of
|
@@ -342,7 +363,9 @@ class Collector:
|
|
342
363
|
set(chain.from_iterable(restricted_objects.values())),
|
343
364
|
)
|
344
365
|
|
345
|
-
def related_objects(
|
366
|
+
def related_objects(
|
367
|
+
self, related_model: Any, related_fields: list[Field], objs: Iterable[Any]
|
368
|
+
) -> QuerySet:
|
346
369
|
"""
|
347
370
|
Get a QuerySet of the related model to objs via related fields.
|
348
371
|
"""
|
@@ -352,7 +375,7 @@ class Collector:
|
|
352
375
|
)
|
353
376
|
return related_model._meta.base_queryset.filter(predicate)
|
354
377
|
|
355
|
-
def sort(self):
|
378
|
+
def sort(self) -> None:
|
356
379
|
sorted_models = []
|
357
380
|
concrete_models = set()
|
358
381
|
models = list(self.data)
|
@@ -370,7 +393,7 @@ class Collector:
|
|
370
393
|
return
|
371
394
|
self.data = {model: self.data[model] for model in sorted_models}
|
372
395
|
|
373
|
-
def delete(self):
|
396
|
+
def delete(self) -> tuple[int, dict[str, int]]:
|
374
397
|
# sort instance collections
|
375
398
|
for model, instances in self.data.items():
|
376
399
|
self.data[model] = sorted(instances, key=attrgetter("id"))
|
plain/models/entrypoints.py
CHANGED
plain/models/enums.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import enum
|
2
4
|
from types import DynamicClassAttribute
|
5
|
+
from typing import Any
|
3
6
|
|
4
7
|
from plain.utils.functional import Promise
|
5
8
|
|
@@ -9,7 +12,13 @@ __all__ = ["Choices", "IntegerChoices", "TextChoices"]
|
|
9
12
|
class ChoicesMeta(enum.EnumMeta):
|
10
13
|
"""A metaclass for creating a enum choices."""
|
11
14
|
|
12
|
-
def __new__(
|
15
|
+
def __new__(
|
16
|
+
metacls: type,
|
17
|
+
classname: str,
|
18
|
+
bases: tuple[type, ...],
|
19
|
+
classdict: Any,
|
20
|
+
**kwds: Any,
|
21
|
+
) -> type:
|
13
22
|
labels = []
|
14
23
|
for key in classdict._member_names:
|
15
24
|
value = classdict[key]
|
@@ -26,33 +35,33 @@ class ChoicesMeta(enum.EnumMeta):
|
|
26
35
|
# Use dict.__setitem__() to suppress defenses against double
|
27
36
|
# assignment in enum's classdict.
|
28
37
|
dict.__setitem__(classdict, key, value)
|
29
|
-
cls = super().__new__(metacls, classname, bases, classdict, **kwds)
|
38
|
+
cls = super().__new__(metacls, classname, bases, classdict, **kwds) # type: ignore[misc]
|
30
39
|
for member, label in zip(cls.__members__.values(), labels):
|
31
40
|
member._label_ = label
|
32
41
|
return enum.unique(cls)
|
33
42
|
|
34
|
-
def __contains__(cls, member):
|
43
|
+
def __contains__(cls, member: object) -> bool:
|
35
44
|
if not isinstance(member, enum.Enum):
|
36
45
|
# Allow non-enums to match against member values.
|
37
46
|
return any(x.value == member for x in cls)
|
38
47
|
return super().__contains__(member)
|
39
48
|
|
40
49
|
@property
|
41
|
-
def names(cls):
|
50
|
+
def names(cls) -> list[str]:
|
42
51
|
empty = ["__empty__"] if hasattr(cls, "__empty__") else []
|
43
52
|
return empty + [member.name for member in cls]
|
44
53
|
|
45
54
|
@property
|
46
|
-
def choices(cls):
|
55
|
+
def choices(cls) -> list[tuple[Any, str]]:
|
47
56
|
empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else []
|
48
57
|
return empty + [(member.value, member.label) for member in cls]
|
49
58
|
|
50
59
|
@property
|
51
|
-
def labels(cls):
|
60
|
+
def labels(cls) -> list[str]:
|
52
61
|
return [label for _, label in cls.choices]
|
53
62
|
|
54
63
|
@property
|
55
|
-
def values(cls):
|
64
|
+
def values(cls) -> list[Any]:
|
56
65
|
return [value for value, _ in cls.choices]
|
57
66
|
|
58
67
|
|
@@ -60,10 +69,10 @@ class Choices(enum.Enum, metaclass=ChoicesMeta):
|
|
60
69
|
"""Class for creating enumerated choices."""
|
61
70
|
|
62
71
|
@DynamicClassAttribute
|
63
|
-
def label(self):
|
72
|
+
def label(self) -> str:
|
64
73
|
return self._label_
|
65
74
|
|
66
|
-
def __str__(self):
|
75
|
+
def __str__(self) -> str:
|
67
76
|
"""
|
68
77
|
Use value when cast to str, so that Choices set as model instance
|
69
78
|
attributes are rendered as expected in templates and similar contexts.
|
@@ -71,7 +80,7 @@ class Choices(enum.Enum, metaclass=ChoicesMeta):
|
|
71
80
|
return str(self.value)
|
72
81
|
|
73
82
|
# A similar format was proposed for Python 3.10.
|
74
|
-
def __repr__(self):
|
83
|
+
def __repr__(self) -> str:
|
75
84
|
return f"{self.__class__.__qualname__}.{self._name_}"
|
76
85
|
|
77
86
|
|
@@ -84,5 +93,7 @@ class IntegerChoices(int, Choices):
|
|
84
93
|
class TextChoices(str, Choices):
|
85
94
|
"""Class for creating enumerated string choices."""
|
86
95
|
|
87
|
-
def _generate_next_value_(
|
96
|
+
def _generate_next_value_(
|
97
|
+
name: str, start: int, count: int, last_values: list[str]
|
98
|
+
) -> str:
|
88
99
|
return name
|