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.
Files changed (105) hide show
  1. plain/models/CHANGELOG.md +13 -0
  2. plain/models/aggregates.py +42 -19
  3. plain/models/backends/base/base.py +125 -105
  4. plain/models/backends/base/client.py +11 -3
  5. plain/models/backends/base/creation.py +22 -12
  6. plain/models/backends/base/features.py +10 -4
  7. plain/models/backends/base/introspection.py +29 -16
  8. plain/models/backends/base/operations.py +187 -91
  9. plain/models/backends/base/schema.py +267 -165
  10. plain/models/backends/base/validation.py +12 -3
  11. plain/models/backends/ddl_references.py +85 -43
  12. plain/models/backends/mysql/base.py +29 -26
  13. plain/models/backends/mysql/client.py +7 -2
  14. plain/models/backends/mysql/compiler.py +12 -3
  15. plain/models/backends/mysql/creation.py +5 -2
  16. plain/models/backends/mysql/features.py +24 -22
  17. plain/models/backends/mysql/introspection.py +22 -13
  18. plain/models/backends/mysql/operations.py +106 -39
  19. plain/models/backends/mysql/schema.py +48 -24
  20. plain/models/backends/mysql/validation.py +13 -6
  21. plain/models/backends/postgresql/base.py +41 -34
  22. plain/models/backends/postgresql/client.py +7 -2
  23. plain/models/backends/postgresql/creation.py +10 -5
  24. plain/models/backends/postgresql/introspection.py +15 -8
  25. plain/models/backends/postgresql/operations.py +109 -42
  26. plain/models/backends/postgresql/schema.py +85 -46
  27. plain/models/backends/sqlite3/_functions.py +151 -115
  28. plain/models/backends/sqlite3/base.py +37 -23
  29. plain/models/backends/sqlite3/client.py +7 -1
  30. plain/models/backends/sqlite3/creation.py +9 -5
  31. plain/models/backends/sqlite3/features.py +5 -3
  32. plain/models/backends/sqlite3/introspection.py +32 -16
  33. plain/models/backends/sqlite3/operations.py +125 -42
  34. plain/models/backends/sqlite3/schema.py +82 -58
  35. plain/models/backends/utils.py +52 -29
  36. plain/models/backups/cli.py +8 -6
  37. plain/models/backups/clients.py +16 -7
  38. plain/models/backups/core.py +24 -13
  39. plain/models/base.py +113 -74
  40. plain/models/cli.py +94 -63
  41. plain/models/config.py +1 -1
  42. plain/models/connections.py +23 -7
  43. plain/models/constraints.py +65 -47
  44. plain/models/database_url.py +1 -1
  45. plain/models/db.py +6 -2
  46. plain/models/deletion.py +66 -43
  47. plain/models/entrypoints.py +1 -1
  48. plain/models/enums.py +22 -11
  49. plain/models/exceptions.py +23 -8
  50. plain/models/expressions.py +440 -257
  51. plain/models/fields/__init__.py +253 -202
  52. plain/models/fields/json.py +120 -54
  53. plain/models/fields/mixins.py +12 -8
  54. plain/models/fields/related.py +284 -252
  55. plain/models/fields/related_descriptors.py +31 -22
  56. plain/models/fields/related_lookups.py +23 -11
  57. plain/models/fields/related_managers.py +81 -47
  58. plain/models/fields/reverse_related.py +58 -55
  59. plain/models/forms.py +89 -63
  60. plain/models/functions/comparison.py +71 -18
  61. plain/models/functions/datetime.py +79 -29
  62. plain/models/functions/math.py +43 -10
  63. plain/models/functions/mixins.py +24 -7
  64. plain/models/functions/text.py +104 -25
  65. plain/models/functions/window.py +12 -6
  66. plain/models/indexes.py +52 -28
  67. plain/models/lookups.py +228 -153
  68. plain/models/migrations/autodetector.py +86 -43
  69. plain/models/migrations/exceptions.py +7 -3
  70. plain/models/migrations/executor.py +33 -7
  71. plain/models/migrations/graph.py +79 -50
  72. plain/models/migrations/loader.py +45 -22
  73. plain/models/migrations/migration.py +23 -18
  74. plain/models/migrations/operations/base.py +37 -19
  75. plain/models/migrations/operations/fields.py +89 -42
  76. plain/models/migrations/operations/models.py +245 -143
  77. plain/models/migrations/operations/special.py +82 -25
  78. plain/models/migrations/optimizer.py +7 -2
  79. plain/models/migrations/questioner.py +58 -31
  80. plain/models/migrations/recorder.py +18 -11
  81. plain/models/migrations/serializer.py +50 -39
  82. plain/models/migrations/state.py +220 -133
  83. plain/models/migrations/utils.py +29 -13
  84. plain/models/migrations/writer.py +17 -14
  85. plain/models/options.py +63 -56
  86. plain/models/otel.py +16 -6
  87. plain/models/preflight.py +35 -12
  88. plain/models/query.py +323 -228
  89. plain/models/query_utils.py +93 -58
  90. plain/models/registry.py +34 -16
  91. plain/models/sql/compiler.py +146 -97
  92. plain/models/sql/datastructures.py +38 -25
  93. plain/models/sql/query.py +255 -169
  94. plain/models/sql/subqueries.py +32 -21
  95. plain/models/sql/where.py +54 -29
  96. plain/models/test/pytest.py +15 -11
  97. plain/models/test/utils.py +4 -2
  98. plain/models/transaction.py +20 -7
  99. plain/models/utils.py +13 -5
  100. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
  101. plain_models-0.50.0.dist-info/RECORD +122 -0
  102. plain_models-0.49.2.dist-info/RECORD +0 -122
  103. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
  104. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
  105. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 = "Constraint “%(name)s is violated."
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, *, name, violation_error_code=None, violation_error_message=None
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(self, model, instance, exclude=None):
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, *, check, name, violation_error_code=None, violation_error_message=None
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(self, model, instance, exclude=None):
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(self, model, instance, exclude=None):
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 = {}
@@ -182,4 +182,4 @@ def build_database_url(config: dict) -> str:
182
182
 
183
183
  path = f"/{name}"
184
184
  url = urlparse.urlunsplit((scheme, netloc, path, query, ""))
185
- return url
185
+ return str(url)
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(list)
109
+ self.field_updates: defaultdict[tuple[Field, Any], list[Any]] = defaultdict(
110
+ list
111
+ )
103
112
  # {model: {field: {instances}}}
104
- self.restricted_objects = defaultdict(partial(defaultdict, set))
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(set) # {model: {models}}
124
+ self.dependencies: defaultdict[Any, set[Any]] = defaultdict(
125
+ set
126
+ ) # {model: {models}}
114
127
 
115
- def add(self, objs, source=None, nullable=False, reverse_dependency=False):
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(self, model, dependency, reverse_dependency=False):
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(self, related_model, related_fields, objs):
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"))
@@ -1,4 +1,4 @@
1
- def setup():
1
+ def setup() -> None:
2
2
  # This package isn't an installed app,
3
3
  # so we need to trigger our own import and cli registration.
4
4
  from .cli import cli # noqa
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__(metacls, classname, bases, classdict, **kwds):
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_(name, start, count, last_values):
96
+ def _generate_next_value_(
97
+ name: str, start: int, count: int, last_values: list[str]
98
+ ) -> str:
88
99
  return name