plain.models 0.49.2__py3-none-any.whl → 0.51.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. plain/models/CHANGELOG.md +27 -0
  2. plain/models/README.md +26 -42
  3. plain/models/__init__.py +2 -0
  4. plain/models/aggregates.py +42 -19
  5. plain/models/backends/base/base.py +125 -105
  6. plain/models/backends/base/client.py +11 -3
  7. plain/models/backends/base/creation.py +24 -14
  8. plain/models/backends/base/features.py +10 -4
  9. plain/models/backends/base/introspection.py +37 -20
  10. plain/models/backends/base/operations.py +187 -91
  11. plain/models/backends/base/schema.py +338 -218
  12. plain/models/backends/base/validation.py +13 -4
  13. plain/models/backends/ddl_references.py +85 -43
  14. plain/models/backends/mysql/base.py +29 -26
  15. plain/models/backends/mysql/client.py +7 -2
  16. plain/models/backends/mysql/compiler.py +13 -4
  17. plain/models/backends/mysql/creation.py +5 -2
  18. plain/models/backends/mysql/features.py +24 -22
  19. plain/models/backends/mysql/introspection.py +22 -13
  20. plain/models/backends/mysql/operations.py +107 -40
  21. plain/models/backends/mysql/schema.py +52 -28
  22. plain/models/backends/mysql/validation.py +13 -6
  23. plain/models/backends/postgresql/base.py +41 -34
  24. plain/models/backends/postgresql/client.py +7 -2
  25. plain/models/backends/postgresql/creation.py +10 -5
  26. plain/models/backends/postgresql/introspection.py +15 -8
  27. plain/models/backends/postgresql/operations.py +110 -43
  28. plain/models/backends/postgresql/schema.py +88 -49
  29. plain/models/backends/sqlite3/_functions.py +151 -115
  30. plain/models/backends/sqlite3/base.py +37 -23
  31. plain/models/backends/sqlite3/client.py +7 -1
  32. plain/models/backends/sqlite3/creation.py +9 -5
  33. plain/models/backends/sqlite3/features.py +5 -3
  34. plain/models/backends/sqlite3/introspection.py +32 -16
  35. plain/models/backends/sqlite3/operations.py +126 -43
  36. plain/models/backends/sqlite3/schema.py +127 -92
  37. plain/models/backends/utils.py +52 -29
  38. plain/models/backups/cli.py +8 -6
  39. plain/models/backups/clients.py +16 -7
  40. plain/models/backups/core.py +24 -13
  41. plain/models/base.py +221 -229
  42. plain/models/cli.py +98 -67
  43. plain/models/config.py +1 -1
  44. plain/models/connections.py +23 -7
  45. plain/models/constraints.py +79 -56
  46. plain/models/database_url.py +1 -1
  47. plain/models/db.py +6 -2
  48. plain/models/deletion.py +80 -56
  49. plain/models/entrypoints.py +1 -1
  50. plain/models/enums.py +22 -11
  51. plain/models/exceptions.py +23 -8
  52. plain/models/expressions.py +441 -258
  53. plain/models/fields/__init__.py +272 -217
  54. plain/models/fields/json.py +123 -57
  55. plain/models/fields/mixins.py +12 -8
  56. plain/models/fields/related.py +324 -290
  57. plain/models/fields/related_descriptors.py +33 -24
  58. plain/models/fields/related_lookups.py +24 -12
  59. plain/models/fields/related_managers.py +102 -79
  60. plain/models/fields/reverse_related.py +66 -63
  61. plain/models/forms.py +101 -75
  62. plain/models/functions/comparison.py +71 -18
  63. plain/models/functions/datetime.py +79 -29
  64. plain/models/functions/math.py +43 -10
  65. plain/models/functions/mixins.py +24 -7
  66. plain/models/functions/text.py +104 -25
  67. plain/models/functions/window.py +12 -6
  68. plain/models/indexes.py +57 -32
  69. plain/models/lookups.py +228 -153
  70. plain/models/meta.py +505 -0
  71. plain/models/migrations/autodetector.py +86 -43
  72. plain/models/migrations/exceptions.py +7 -3
  73. plain/models/migrations/executor.py +33 -7
  74. plain/models/migrations/graph.py +79 -50
  75. plain/models/migrations/loader.py +45 -22
  76. plain/models/migrations/migration.py +23 -18
  77. plain/models/migrations/operations/base.py +38 -20
  78. plain/models/migrations/operations/fields.py +95 -48
  79. plain/models/migrations/operations/models.py +246 -142
  80. plain/models/migrations/operations/special.py +82 -25
  81. plain/models/migrations/optimizer.py +7 -2
  82. plain/models/migrations/questioner.py +58 -31
  83. plain/models/migrations/recorder.py +27 -16
  84. plain/models/migrations/serializer.py +50 -39
  85. plain/models/migrations/state.py +232 -156
  86. plain/models/migrations/utils.py +30 -14
  87. plain/models/migrations/writer.py +17 -14
  88. plain/models/options.py +189 -518
  89. plain/models/otel.py +16 -6
  90. plain/models/preflight.py +42 -17
  91. plain/models/query.py +400 -251
  92. plain/models/query_utils.py +109 -69
  93. plain/models/registry.py +40 -21
  94. plain/models/sql/compiler.py +190 -127
  95. plain/models/sql/datastructures.py +38 -25
  96. plain/models/sql/query.py +320 -225
  97. plain/models/sql/subqueries.py +36 -25
  98. plain/models/sql/where.py +54 -29
  99. plain/models/test/pytest.py +15 -11
  100. plain/models/test/utils.py +4 -2
  101. plain/models/transaction.py +20 -7
  102. plain/models/utils.py +17 -6
  103. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
  104. plain_models-0.51.0.dist-info/RECORD +123 -0
  105. plain_models-0.49.2.dist-info/RECORD +0 -122
  106. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
  107. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
  108. {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
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,
@@ -9,25 +13,29 @@ from plain.models import (
9
13
  transaction,
10
14
  )
11
15
  from plain.models.db import IntegrityError, db_connection
16
+ from plain.models.meta import Meta
12
17
  from plain.models.query import QuerySet
13
18
 
19
+ if TYPE_CHECKING:
20
+ from plain.models.fields import Field
21
+
14
22
 
15
23
  class ProtectedError(IntegrityError):
16
- def __init__(self, msg, protected_objects):
24
+ def __init__(self, msg: str, protected_objects: Iterable[Any]) -> None:
17
25
  self.protected_objects = protected_objects
18
26
  super().__init__(msg, protected_objects)
19
27
 
20
28
 
21
29
  class RestrictedError(IntegrityError):
22
- def __init__(self, msg, restricted_objects):
30
+ def __init__(self, msg: str, restricted_objects: Iterable[Any]) -> None:
23
31
  self.restricted_objects = restricted_objects
24
32
  super().__init__(msg, restricted_objects)
25
33
 
26
34
 
27
- def CASCADE(collector, field, sub_objs):
35
+ def CASCADE(collector: Collector, field: Field, sub_objs: Any) -> None:
28
36
  collector.collect(
29
37
  sub_objs,
30
- source=field.remote_field.model,
38
+ source=field.remote_field.model, # type: ignore[attr-defined]
31
39
  nullable=field.allow_null,
32
40
  fail_on_restricted=False,
33
41
  )
@@ -35,84 +43,96 @@ def CASCADE(collector, field, sub_objs):
35
43
  collector.add_field_update(field, None, sub_objs)
36
44
 
37
45
 
38
- def PROTECT(collector, field, sub_objs):
46
+ def PROTECT(collector: Collector, field: Field, sub_objs: Any) -> None:
39
47
  raise ProtectedError(
40
- f"Cannot delete some instances of model '{field.remote_field.model.__name__}' because they are "
48
+ f"Cannot delete some instances of model '{field.remote_field.model.__name__}' because they are " # type: ignore[attr-defined]
41
49
  f"referenced through a protected foreign key: '{sub_objs[0].__class__.__name__}.{field.name}'",
42
50
  sub_objs,
43
51
  )
44
52
 
45
53
 
46
- def RESTRICT(collector, field, sub_objs):
54
+ def RESTRICT(collector: Collector, field: Field, sub_objs: Any) -> None:
47
55
  collector.add_restricted_objects(field, sub_objs)
48
- collector.add_dependency(field.remote_field.model, field.model)
56
+ collector.add_dependency(field.remote_field.model, field.model) # type: ignore[attr-defined]
49
57
 
50
58
 
51
- def SET(value):
59
+ def SET(value: Any) -> Callable[[Collector, Field, Any], None]:
52
60
  if callable(value):
53
61
 
54
- def set_on_delete(collector, field, sub_objs):
62
+ def set_on_delete(collector: Collector, field: Field, sub_objs: Any) -> None:
55
63
  collector.add_field_update(field, value(), sub_objs)
56
64
 
57
65
  else:
58
66
 
59
- def set_on_delete(collector, field, sub_objs):
67
+ def set_on_delete(collector: Collector, field: Field, sub_objs: Any) -> None:
60
68
  collector.add_field_update(field, value, sub_objs)
61
69
 
62
- set_on_delete.deconstruct = lambda: ("plain.models.SET", (value,), {})
63
- set_on_delete.lazy_sub_objs = True
70
+ set_on_delete.deconstruct = lambda: ("plain.models.SET", (value,), {}) # type: ignore[attr-defined]
71
+ set_on_delete.lazy_sub_objs = True # type: ignore[attr-defined]
64
72
  return set_on_delete
65
73
 
66
74
 
67
- def SET_NULL(collector, field, sub_objs):
75
+ def SET_NULL(collector: Collector, field: Field, sub_objs: Any) -> None:
68
76
  collector.add_field_update(field, None, sub_objs)
69
77
 
70
78
 
71
- SET_NULL.lazy_sub_objs = True
79
+ SET_NULL.lazy_sub_objs = True # type: ignore[attr-defined]
72
80
 
73
81
 
74
- def SET_DEFAULT(collector, field, sub_objs):
82
+ def SET_DEFAULT(collector: Collector, field: Field, sub_objs: Any) -> None:
75
83
  collector.add_field_update(field, field.get_default(), sub_objs)
76
84
 
77
85
 
78
- SET_DEFAULT.lazy_sub_objs = True
86
+ SET_DEFAULT.lazy_sub_objs = True # type: ignore[attr-defined]
79
87
 
80
88
 
81
- def DO_NOTHING(collector, field, sub_objs):
89
+ def DO_NOTHING(collector: Collector, field: Field, sub_objs: Any) -> None:
82
90
  pass
83
91
 
84
92
 
85
- def get_candidate_relations_to_delete(opts):
93
+ def get_candidate_relations_to_delete(meta: Meta) -> Generator[Any, None, None]:
86
94
  # The candidate relations are the ones that come from N-1 and 1-1 relations.
87
95
  # N-N (i.e., many-to-many) relations aren't candidates for deletion.
88
96
  return (
89
97
  f
90
- for f in opts.get_fields(include_hidden=True)
98
+ for f in meta.get_fields(include_hidden=True)
91
99
  if f.auto_created and not f.concrete and f.one_to_many
92
100
  )
93
101
 
94
102
 
95
103
  class Collector:
96
- def __init__(self, origin=None):
104
+ def __init__(self, origin: Any = None) -> None:
97
105
  # A Model or QuerySet object.
98
106
  self.origin = origin
99
107
  # Initially, {model: {instances}}, later values become lists.
100
- self.data = defaultdict(set)
108
+ self.data: defaultdict[Any, Any] = defaultdict(set)
101
109
  # {(field, value): [instances, …]}
102
- self.field_updates = defaultdict(list)
110
+ self.field_updates: defaultdict[tuple[Field, Any], list[Any]] = defaultdict(
111
+ list
112
+ )
103
113
  # {model: {field: {instances}}}
104
- self.restricted_objects = defaultdict(partial(defaultdict, set))
114
+ self.restricted_objects: defaultdict[Any, Any] = defaultdict(
115
+ partial(defaultdict, set)
116
+ )
105
117
  # fast_deletes is a list of queryset-likes that can be deleted without
106
118
  # fetching the objects into memory.
107
- self.fast_deletes = []
119
+ self.fast_deletes: list[Any] = []
108
120
 
109
121
  # Tracks deletion-order dependency for databases without transactions
110
122
  # or ability to defer constraint checks. Only concrete model classes
111
123
  # should be included, as the dependencies exist only between actual
112
124
  # database tables.
113
- self.dependencies = defaultdict(set) # {model: {models}}
125
+ self.dependencies: defaultdict[Any, set[Any]] = defaultdict(
126
+ set
127
+ ) # {model: {models}}
114
128
 
115
- def add(self, objs, source=None, nullable=False, reverse_dependency=False):
129
+ def add(
130
+ self,
131
+ objs: Iterable[Any],
132
+ source: Any = None,
133
+ nullable: bool = False,
134
+ reverse_dependency: bool = False,
135
+ ) -> list[Any]:
116
136
  """
117
137
  Add 'objs' to the collection of objects to be deleted. If the call is
118
138
  the result of a cascade, 'source' should be the model that caused it,
@@ -136,32 +156,34 @@ class Collector:
136
156
  self.add_dependency(source, model, reverse_dependency=reverse_dependency)
137
157
  return new_objs
138
158
 
139
- def add_dependency(self, model, dependency, reverse_dependency=False):
159
+ def add_dependency(
160
+ self, model: Any, dependency: Any, reverse_dependency: bool = False
161
+ ) -> None:
140
162
  if reverse_dependency:
141
163
  model, dependency = dependency, model
142
164
  self.dependencies[model].add(dependency)
143
165
  self.data.setdefault(dependency, self.data.default_factory())
144
166
 
145
- def add_field_update(self, field, value, objs):
167
+ def add_field_update(self, field: Field, value: Any, objs: Iterable[Any]) -> None:
146
168
  """
147
169
  Schedule a field update. 'objs' must be a homogeneous iterable
148
170
  collection of model instances (e.g. a QuerySet).
149
171
  """
150
172
  self.field_updates[field, value].append(objs)
151
173
 
152
- def add_restricted_objects(self, field, objs):
174
+ def add_restricted_objects(self, field: Field, objs: Iterable[Any]) -> None:
153
175
  if objs:
154
176
  model = objs[0].__class__
155
177
  self.restricted_objects[model][field].update(objs)
156
178
 
157
- def clear_restricted_objects_from_set(self, model, objs):
179
+ def clear_restricted_objects_from_set(self, model: Any, objs: set[Any]) -> None:
158
180
  if model in self.restricted_objects:
159
181
  self.restricted_objects[model] = {
160
182
  field: items - objs
161
183
  for field, items in self.restricted_objects[model].items()
162
184
  }
163
185
 
164
- def clear_restricted_objects_from_queryset(self, model, qs):
186
+ def clear_restricted_objects_from_queryset(self, model: Any, qs: QuerySet) -> None:
165
187
  if model in self.restricted_objects:
166
188
  objs = set(
167
189
  qs.filter(
@@ -174,7 +196,7 @@ class Collector:
174
196
  )
175
197
  self.clear_restricted_objects_from_set(model, objs)
176
198
 
177
- def can_fast_delete(self, objs, from_field=None):
199
+ def can_fast_delete(self, objs: Any, from_field: Any = None) -> bool:
178
200
  """
179
201
  Determine if the objects in the given queryset-like or single object
180
202
  can be fast-deleted. This can be done if there are no cascades, no
@@ -187,8 +209,8 @@ class Collector:
187
209
  """
188
210
  if from_field and from_field.remote_field.on_delete is not CASCADE:
189
211
  return False
190
- if hasattr(objs, "_meta"):
191
- model = objs._meta.model
212
+ if hasattr(objs, "_model_meta"):
213
+ model = objs._model_meta.model
192
214
  elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"):
193
215
  model = objs.model
194
216
  else:
@@ -196,16 +218,16 @@ class Collector:
196
218
 
197
219
  # The use of from_field comes from the need to avoid cascade back to
198
220
  # parent when parent delete is cascading to child.
199
- opts = model._meta
221
+ meta = model._model_meta
200
222
  return (
201
223
  # Foreign keys pointing to this model.
202
224
  all(
203
225
  related.field.remote_field.on_delete is DO_NOTHING
204
- for related in get_candidate_relations_to_delete(opts)
226
+ for related in get_candidate_relations_to_delete(meta)
205
227
  )
206
228
  )
207
229
 
208
- def get_del_batches(self, objs, fields):
230
+ def get_del_batches(self, objs: list[Any], fields: list[Field]) -> list[list[Any]]:
209
231
  """
210
232
  Return the objs in suitably sized batches for the used db_connection.
211
233
  """
@@ -224,13 +246,13 @@ class Collector:
224
246
 
225
247
  def collect(
226
248
  self,
227
- objs,
228
- source=None,
229
- nullable=False,
230
- collect_related=True,
231
- reverse_dependency=False,
232
- fail_on_restricted=True,
233
- ):
249
+ objs: Iterable[Any],
250
+ source: Any = None,
251
+ nullable: bool = False,
252
+ collect_related: bool = True,
253
+ reverse_dependency: bool = False,
254
+ fail_on_restricted: bool = True,
255
+ ) -> None:
234
256
  """
235
257
  Add 'objs' to the collection of objects to be deleted as well as all
236
258
  parent instances. 'objs' must be a homogeneous iterable collection of
@@ -268,7 +290,7 @@ class Collector:
268
290
 
269
291
  model_fast_deletes = defaultdict(list)
270
292
  protected_objects = defaultdict(list)
271
- for related in get_candidate_relations_to_delete(model._meta):
293
+ for related in get_candidate_relations_to_delete(model._model_meta):
272
294
  field = related.field
273
295
  on_delete = field.remote_field.on_delete
274
296
  if on_delete == DO_NOTHING:
@@ -291,7 +313,7 @@ class Collector:
291
313
  chain.from_iterable(
292
314
  (rf.attname for rf in rel.field.foreign_related_fields)
293
315
  for rel in get_candidate_relations_to_delete(
294
- related_model._meta
316
+ related_model._model_meta
295
317
  )
296
318
  )
297
319
  )
@@ -342,7 +364,9 @@ class Collector:
342
364
  set(chain.from_iterable(restricted_objects.values())),
343
365
  )
344
366
 
345
- def related_objects(self, related_model, related_fields, objs):
367
+ def related_objects(
368
+ self, related_model: Any, related_fields: list[Field], objs: Iterable[Any]
369
+ ) -> QuerySet:
346
370
  """
347
371
  Get a QuerySet of the related model to objs via related fields.
348
372
  """
@@ -350,9 +374,9 @@ class Collector:
350
374
  [(f"{related_field.name}__in", objs) for related_field in related_fields],
351
375
  connector=query_utils.Q.OR,
352
376
  )
353
- return related_model._meta.base_queryset.filter(predicate)
377
+ return related_model._model_meta.base_queryset.filter(predicate)
354
378
 
355
- def sort(self):
379
+ def sort(self) -> None:
356
380
  sorted_models = []
357
381
  concrete_models = set()
358
382
  models = list(self.data)
@@ -370,7 +394,7 @@ class Collector:
370
394
  return
371
395
  self.data = {model: self.data[model] for model in sorted_models}
372
396
 
373
- def delete(self):
397
+ def delete(self) -> tuple[int, dict[str, int]]:
374
398
  # sort instance collections
375
399
  for model, instances in self.data.items():
376
400
  self.data[model] = sorted(instances, key=attrgetter("id"))
@@ -388,15 +412,15 @@ class Collector:
388
412
  if self.can_fast_delete(instance):
389
413
  with transaction.mark_for_rollback_on_error():
390
414
  count = sql.DeleteQuery(model).delete_batch([instance.id])
391
- setattr(instance, model._meta.get_field("id").attname, None)
392
- return count, {model._meta.label: count}
415
+ setattr(instance, model._model_meta.get_field("id").attname, None)
416
+ return count, {model.model_options.label: count}
393
417
 
394
418
  with transaction.atomic(savepoint=False):
395
419
  # fast deletes
396
420
  for qs in self.fast_deletes:
397
421
  count = qs._raw_delete()
398
422
  if count:
399
- deleted_counter[qs.model._meta.label] += count
423
+ deleted_counter[qs.model.model_options.label] += count
400
424
 
401
425
  # update fields
402
426
  for (field, value), instances_list in self.field_updates.items():
@@ -430,9 +454,9 @@ class Collector:
430
454
  id_list = [obj.id for obj in instances]
431
455
  count = query.delete_batch(id_list)
432
456
  if count:
433
- deleted_counter[model._meta.label] += count
457
+ deleted_counter[model.model_options.label] += count
434
458
 
435
459
  for model, instances in self.data.items():
436
460
  for instance in instances:
437
- setattr(instance, model._meta.get_field("id").attname, None)
461
+ setattr(instance, model._model_meta.get_field("id").attname, None)
438
462
  return sum(deleted_counter.values()), dict(deleted_counter)
@@ -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
@@ -1,4 +1,12 @@
1
- from typing import Any
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any, TypeVar
5
+
6
+ if TYPE_CHECKING:
7
+ from plain.models.backends.base.base import BaseDatabaseWrapper
8
+
9
+ F = TypeVar("F", bound=Callable[..., Any])
2
10
 
3
11
  # MARK: Database Query Exceptions
4
12
 
@@ -152,7 +160,7 @@ class DatabaseErrorWrapper:
152
160
  exceptions using Plain's common wrappers.
153
161
  """
154
162
 
155
- def __init__(self, wrapper):
163
+ def __init__(self, wrapper: BaseDatabaseWrapper) -> None:
156
164
  """
157
165
  wrapper is a database wrapper.
158
166
 
@@ -160,10 +168,15 @@ class DatabaseErrorWrapper:
160
168
  """
161
169
  self.wrapper = wrapper
162
170
 
163
- def __enter__(self):
171
+ def __enter__(self) -> None:
164
172
  pass
165
173
 
166
- def __exit__(self, exc_type, exc_value, traceback):
174
+ def __exit__(
175
+ self,
176
+ exc_type: type[BaseException] | None,
177
+ exc_value: BaseException | None,
178
+ traceback: Any,
179
+ ) -> None:
167
180
  if exc_type is None:
168
181
  return
169
182
  for plain_exc_type in (
@@ -179,18 +192,20 @@ class DatabaseErrorWrapper:
179
192
  ):
180
193
  db_exc_type = getattr(self.wrapper.Database, plain_exc_type.__name__)
181
194
  if issubclass(exc_type, db_exc_type):
182
- plain_exc_value = plain_exc_type(*exc_value.args)
195
+ plain_exc_value = (
196
+ plain_exc_type(*exc_value.args) if exc_value else plain_exc_type()
197
+ )
183
198
  # Only set the 'errors_occurred' flag for errors that may make
184
199
  # the connection unusable.
185
200
  if plain_exc_type not in (DataError, IntegrityError):
186
201
  self.wrapper.errors_occurred = True
187
202
  raise plain_exc_value.with_traceback(traceback) from exc_value
188
203
 
189
- def __call__(self, func):
204
+ def __call__(self, func: F) -> F:
190
205
  # Note that we are intentionally not using @wraps here for performance
191
206
  # reasons. Refs #21109.
192
- def inner(*args, **kwargs):
207
+ def inner(*args: Any, **kwargs: Any) -> Any:
193
208
  with self:
194
209
  return func(*args, **kwargs)
195
210
 
196
- return inner
211
+ return inner # type: ignore[return-value]