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,30 +1,39 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
4
  import re
3
5
  from collections import namedtuple
6
+ from collections.abc import Generator
7
+ from typing import TYPE_CHECKING, Any
4
8
 
5
9
  from plain.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
6
10
 
11
+ if TYPE_CHECKING:
12
+ from plain.models.fields import Field
13
+
7
14
  FieldReference = namedtuple("FieldReference", "to through")
8
15
 
9
16
  COMPILED_REGEX_TYPE = type(re.compile(""))
10
17
 
11
18
 
12
19
  class RegexObject:
13
- def __init__(self, obj):
20
+ def __init__(self, obj: Any) -> None:
14
21
  self.pattern = obj.pattern
15
22
  self.flags = obj.flags
16
23
 
17
- def __eq__(self, other):
24
+ def __eq__(self, other: Any) -> bool:
18
25
  if not isinstance(other, RegexObject):
19
26
  return NotImplemented
20
27
  return self.pattern == other.pattern and self.flags == other.flags
21
28
 
22
29
 
23
- def get_migration_name_timestamp():
30
+ def get_migration_name_timestamp() -> str:
24
31
  return datetime.datetime.now().strftime("%Y%m%d_%H%M")
25
32
 
26
33
 
27
- def resolve_relation(model, package_label=None, model_name=None):
34
+ def resolve_relation(
35
+ model: str | Any, package_label: str | None = None, model_name: str | None = None
36
+ ) -> tuple[str, str]:
28
37
  """
29
38
  Turn a model class or model reference string and return a model tuple.
30
39
 
@@ -51,12 +60,12 @@ def resolve_relation(model, package_label=None, model_name=None):
51
60
 
52
61
 
53
62
  def field_references(
54
- model_tuple,
55
- field,
56
- reference_model_tuple,
57
- reference_field_name=None,
58
- reference_field=None,
59
- ):
63
+ model_tuple: tuple[str, str],
64
+ field: Field,
65
+ reference_model_tuple: tuple[str, str],
66
+ reference_field_name: str | None = None,
67
+ reference_field: Field | None = None,
68
+ ) -> FieldReference | bool:
60
69
  """
61
70
  Return either False or a FieldReference if `field` references provided
62
71
  context.
@@ -97,7 +106,9 @@ def field_references(
97
106
  return FieldReference(references_to, references_through)
98
107
 
99
108
 
100
- def get_references(state, model_tuple, field_tuple=()):
109
+ def get_references(
110
+ state: Any, model_tuple: tuple[str, str], field_tuple: tuple[Any, ...] = ()
111
+ ) -> Generator[tuple[Any, str, Field, FieldReference], None, None]:
101
112
  """
102
113
  Generator of (model_state, name, field, reference) referencing
103
114
  provided context.
@@ -108,12 +119,17 @@ def get_references(state, model_tuple, field_tuple=()):
108
119
  for state_model_tuple, model_state in state.models.items():
109
120
  for name, field in model_state.fields.items():
110
121
  reference = field_references(
111
- state_model_tuple, field, model_tuple, *field_tuple
122
+ state_model_tuple,
123
+ field,
124
+ model_tuple,
125
+ *field_tuple, # type: ignore[arg-type]
112
126
  )
113
127
  if reference:
114
128
  yield model_state, name, field, reference
115
129
 
116
130
 
117
- def field_is_referenced(state, model_tuple, field_tuple):
131
+ def field_is_referenced(
132
+ state: Any, model_tuple: tuple[str, str], field_tuple: tuple[Any, ...]
133
+ ) -> bool:
118
134
  """Return whether `field_tuple` is referenced by any state models."""
119
135
  return next(get_references(state, model_tuple, field_tuple), None) is not None
@@ -1,6 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import re
3
5
  from importlib import import_module
6
+ from typing import Any
4
7
 
5
8
  from plain.models import migrations
6
9
  from plain.models.migrations.loader import MigrationLoader
@@ -14,13 +17,13 @@ from plain.utils.timezone import now
14
17
 
15
18
 
16
19
  class OperationWriter:
17
- def __init__(self, operation, indentation=2):
20
+ def __init__(self, operation: Any, indentation: int = 2) -> None:
18
21
  self.operation = operation
19
- self.buff = []
22
+ self.buff: list[str] = []
20
23
  self.indentation = indentation
21
24
 
22
- def serialize(self):
23
- def _write(_arg_name, _arg_value):
25
+ def serialize(self) -> tuple[str, set[str]]:
26
+ def _write(_arg_name: str, _arg_value: Any) -> None:
24
27
  if _arg_name in self.operation.serialization_expand_args and isinstance(
25
28
  _arg_value, list | tuple | dict
26
29
  ):
@@ -100,16 +103,16 @@ class OperationWriter:
100
103
  self.feed("),")
101
104
  return self.render(), imports
102
105
 
103
- def indent(self):
106
+ def indent(self) -> None:
104
107
  self.indentation += 1
105
108
 
106
- def unindent(self):
109
+ def unindent(self) -> None:
107
110
  self.indentation -= 1
108
111
 
109
- def feed(self, line):
112
+ def feed(self, line: str) -> None:
110
113
  self.buff.append(" " * (self.indentation * 4) + line)
111
114
 
112
- def render(self):
115
+ def render(self) -> str:
113
116
  return "\n".join(self.buff)
114
117
 
115
118
 
@@ -119,12 +122,12 @@ class MigrationWriter:
119
122
  of the migration file from it.
120
123
  """
121
124
 
122
- def __init__(self, migration, include_header=True):
125
+ def __init__(self, migration: Any, include_header: bool = True) -> None:
123
126
  self.migration = migration
124
127
  self.include_header = include_header
125
128
  self.needs_manual_porting = False
126
129
 
127
- def as_string(self):
130
+ def as_string(self) -> str:
128
131
  """Return a string of the file contents."""
129
132
  items = {
130
133
  "replaces_str": "",
@@ -198,7 +201,7 @@ class MigrationWriter:
198
201
  return MIGRATION_TEMPLATE % items
199
202
 
200
203
  @property
201
- def basedir(self):
204
+ def basedir(self) -> str:
202
205
  migrations_package_name, _ = MigrationLoader.migrations_module(
203
206
  self.migration.package_label
204
207
  )
@@ -266,15 +269,15 @@ class MigrationWriter:
266
269
  return final_dir
267
270
 
268
271
  @property
269
- def filename(self):
272
+ def filename(self) -> str:
270
273
  return f"{self.migration.name}.py"
271
274
 
272
275
  @property
273
- def path(self):
276
+ def path(self) -> str:
274
277
  return os.path.join(self.basedir, self.filename)
275
278
 
276
279
  @classmethod
277
- def serialize(cls, value):
280
+ def serialize(cls, value: Any) -> tuple[str, set[str]]:
278
281
  return serializer_factory(value).serialize()
279
282
 
280
283
 
plain/models/options.py CHANGED
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import bisect
2
4
  import inspect
3
5
  from collections import defaultdict
4
6
  from functools import cached_property
7
+ from typing import TYPE_CHECKING, Any
5
8
 
6
9
  from plain.models import models_registry
7
10
  from plain.models.constraints import UniqueConstraint
@@ -10,6 +13,10 @@ from plain.models.exceptions import FieldDoesNotExist
10
13
  from plain.models.query import QuerySet
11
14
  from plain.utils.datastructures import ImmutableList
12
15
 
16
+ if TYPE_CHECKING:
17
+ from plain.models.backends.base.base import BaseDatabaseWrapper
18
+ from plain.models.fields import Field
19
+
13
20
  PROXY_PARENTS = object()
14
21
 
15
22
  EMPTY_RELATION_TREE = ()
@@ -33,7 +40,7 @@ DEFAULT_NAMES = (
33
40
  )
34
41
 
35
42
 
36
- def make_immutable_fields_list(name, data):
43
+ def make_immutable_fields_list(name: str, data: Any) -> ImmutableList:
37
44
  return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
38
45
 
39
46
 
@@ -52,43 +59,43 @@ class Options:
52
59
 
53
60
  default_models_registry = models_registry
54
61
 
55
- def __init__(self, meta, package_label=None):
56
- self._get_fields_cache = {}
57
- self.local_fields = []
58
- self.local_many_to_many = []
59
- self.queryset_class = None
60
- self.model_name = None
61
- self.db_table = ""
62
- self.db_table_comment = ""
63
- self.ordering = []
64
- self.indexes = []
65
- self.constraints = []
66
- self.object_name = None
67
- self.package_label = package_label
68
- self.required_db_features = []
69
- self.required_db_vendor = None
70
- self.meta = meta
62
+ def __init__(self, meta: Any, package_label: str | None = None):
63
+ self._get_fields_cache: dict[tuple[bool, bool, bool, bool], Any] = {}
64
+ self.local_fields: list[Field] = []
65
+ self.local_many_to_many: list[Field] = []
66
+ self.queryset_class: type[QuerySet] | None = None
67
+ self.model_name: str | None = None
68
+ self.db_table: str = ""
69
+ self.db_table_comment: str = ""
70
+ self.ordering: list[Any] = []
71
+ self.indexes: list[Any] = []
72
+ self.constraints: list[Any] = []
73
+ self.object_name: str | None = None
74
+ self.package_label: str | None = package_label
75
+ self.required_db_features: list[str] = []
76
+ self.required_db_vendor: str | None = None
77
+ self.meta: Any = meta
71
78
 
72
79
  # List of all lookups defined in ForeignKey 'limit_choices_to' options
73
80
  # from *other* models. Needed for some admin checks. Internal use only.
74
- self.related_fkey_lookups = []
81
+ self.related_fkey_lookups: list[Any] = []
75
82
 
76
83
  # A custom app registry to use, if you're making a separate model set.
77
- self.models_registry = self.default_models_registry
84
+ self.models_registry: Any = self.default_models_registry
78
85
 
79
86
  @property
80
- def label(self):
87
+ def label(self) -> str:
81
88
  return f"{self.package_label}.{self.object_name}"
82
89
 
83
90
  @property
84
- def label_lower(self):
91
+ def label_lower(self) -> str:
85
92
  return f"{self.package_label}.{self.model_name}"
86
93
 
87
- def contribute_to_class(self, cls, name):
94
+ def contribute_to_class(self, cls: type[Any], name: str) -> None:
88
95
  from plain.models.backends.utils import truncate_name
89
96
 
90
97
  cls._meta = self
91
- self.model = cls
98
+ self.model: type[Any] = cls
92
99
  # First, construct the default values for these options.
93
100
  self.object_name = cls.__name__
94
101
  self.model_name = self.object_name.lower()
@@ -138,7 +145,7 @@ class Options:
138
145
  db_connection.ops.max_name_length(),
139
146
  )
140
147
 
141
- def _format_names_with_class(self, cls, objs):
148
+ def _format_names_with_class(self, cls: type[Any], objs: list[Any]) -> list[Any]:
142
149
  """Package label/class name interpolation for object names."""
143
150
  new_objs = []
144
151
  for obj in objs:
@@ -150,7 +157,7 @@ class Options:
150
157
  new_objs.append(obj)
151
158
  return new_objs
152
159
 
153
- def add_field(self, field):
160
+ def add_field(self, field: Field) -> None:
154
161
  # Insert the given field in the order in which it was created, using
155
162
  # the "creation_counter" attribute of the field.
156
163
  # Move many-to-many related fields from self.fields into
@@ -181,13 +188,13 @@ class Options:
181
188
  else:
182
189
  self._expire_cache(reverse=False)
183
190
 
184
- def __repr__(self):
191
+ def __repr__(self) -> str:
185
192
  return f"<Options for {self.object_name}>"
186
193
 
187
- def __str__(self):
194
+ def __str__(self) -> str:
188
195
  return self.label_lower
189
196
 
190
- def can_migrate(self, connection):
197
+ def can_migrate(self, connection: BaseDatabaseWrapper) -> bool:
191
198
  """
192
199
  Return True if the model can/should be migrated on the given
193
200
  `connection` object.
@@ -202,7 +209,7 @@ class Options:
202
209
  return True
203
210
 
204
211
  @property
205
- def base_queryset(self):
212
+ def base_queryset(self) -> QuerySet:
206
213
  """
207
214
  The base queryset is used by Plain's internal operations like cascading
208
215
  deletes, migrations, and related object lookups. It provides access to
@@ -216,13 +223,13 @@ class Options:
216
223
  return QuerySet(model=self.model)
217
224
 
218
225
  @property
219
- def queryset(self):
226
+ def queryset(self) -> QuerySet:
220
227
  if self.queryset_class:
221
228
  return self.queryset_class(model=self.model)
222
229
  return QuerySet(model=self.model)
223
230
 
224
231
  @cached_property
225
- def fields(self):
232
+ def fields(self) -> ImmutableList:
226
233
  """
227
234
  Return a list of all forward fields on the model and its parents,
228
235
  excluding ManyToManyFields.
@@ -239,13 +246,13 @@ class Options:
239
246
  # use that property directly because related_model is a cached property,
240
247
  # and all the models may not have been loaded yet; we don't want to cache
241
248
  # the string reference to the related_model.
242
- def is_not_an_m2m_field(f):
249
+ def is_not_an_m2m_field(f: Any) -> bool:
243
250
  return not (f.is_relation and f.many_to_many)
244
251
 
245
- def is_not_a_generic_relation(f):
252
+ def is_not_a_generic_relation(f: Any) -> bool:
246
253
  return not (f.is_relation and f.one_to_many)
247
254
 
248
- def is_not_a_generic_foreign_key(f):
255
+ def is_not_a_generic_foreign_key(f: Any) -> bool:
249
256
  return not (
250
257
  f.is_relation
251
258
  and f.many_to_one
@@ -264,7 +271,7 @@ class Options:
264
271
  )
265
272
 
266
273
  @cached_property
267
- def concrete_fields(self):
274
+ def concrete_fields(self) -> ImmutableList:
268
275
  """
269
276
  Return a list of all concrete fields on the model and its parents.
270
277
 
@@ -277,7 +284,7 @@ class Options:
277
284
  )
278
285
 
279
286
  @cached_property
280
- def local_concrete_fields(self):
287
+ def local_concrete_fields(self) -> ImmutableList:
281
288
  """
282
289
  Return a list of all concrete fields on the model.
283
290
 
@@ -290,7 +297,7 @@ class Options:
290
297
  )
291
298
 
292
299
  @cached_property
293
- def many_to_many(self):
300
+ def many_to_many(self) -> ImmutableList:
294
301
  """
295
302
  Return a list of all many to many fields on the model and its parents.
296
303
 
@@ -308,7 +315,7 @@ class Options:
308
315
  )
309
316
 
310
317
  @cached_property
311
- def related_objects(self):
318
+ def related_objects(self) -> ImmutableList:
312
319
  """
313
320
  Return all related objects pointing to the current model. The related
314
321
  objects can come from a one-to-one, one-to-many, or many-to-many field
@@ -331,7 +338,7 @@ class Options:
331
338
  )
332
339
 
333
340
  @cached_property
334
- def _forward_fields_map(self):
341
+ def _forward_fields_map(self) -> dict[str, Any]:
335
342
  res = {}
336
343
  fields = self._get_fields(reverse=False)
337
344
  for field in fields:
@@ -346,7 +353,7 @@ class Options:
346
353
  return res
347
354
 
348
355
  @cached_property
349
- def fields_map(self):
356
+ def fields_map(self) -> dict[str, Any]:
350
357
  res = {}
351
358
  fields = self._get_fields(forward=False, include_hidden=True)
352
359
  for field in fields:
@@ -360,7 +367,7 @@ class Options:
360
367
  pass
361
368
  return res
362
369
 
363
- def get_field(self, field_name):
370
+ def get_field(self, field_name: str) -> Any:
364
371
  """
365
372
  Return a field instance given the name of a forward or reverse field.
366
373
  """
@@ -387,14 +394,14 @@ class Options:
387
394
  f"{self.object_name} has no field named '{field_name}'"
388
395
  )
389
396
 
390
- def _populate_directed_relation_graph(self):
397
+ def _populate_directed_relation_graph(self) -> Any:
391
398
  """
392
399
  This method is used by each model to find its reverse objects. As this
393
400
  method is very expensive and is accessed frequently (it looks up every
394
401
  field in a model, in every app), it is computed on first access and then
395
402
  is set as a property on every model.
396
403
  """
397
- related_objects_graph = defaultdict(list)
404
+ related_objects_graph: defaultdict[str, list[Any]] = defaultdict(list)
398
405
 
399
406
  all_models = self.models_registry.get_models()
400
407
  for model in all_models:
@@ -423,10 +430,10 @@ class Options:
423
430
  return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE)
424
431
 
425
432
  @cached_property
426
- def _relation_tree(self):
433
+ def _relation_tree(self) -> Any:
427
434
  return self._populate_directed_relation_graph()
428
435
 
429
- def _expire_cache(self, forward=True, reverse=True):
436
+ def _expire_cache(self, forward: bool = True, reverse: bool = True) -> None:
430
437
  # This method is usually called by packages.cache_clear(), when the
431
438
  # registry is finalized, or when a new field is added.
432
439
  if forward:
@@ -439,7 +446,7 @@ class Options:
439
446
  delattr(self, cache_key)
440
447
  self._get_fields_cache = {}
441
448
 
442
- def get_fields(self, include_hidden=False):
449
+ def get_fields(self, include_hidden: bool = False) -> ImmutableList:
443
450
  """
444
451
  Return a list of fields associated to the model. By default, include
445
452
  forward and reverse fields, fields derived from inheritance, but not
@@ -452,11 +459,11 @@ class Options:
452
459
 
453
460
  def _get_fields(
454
461
  self,
455
- forward=True,
456
- reverse=True,
457
- include_hidden=False,
458
- seen_models=None,
459
- ):
462
+ forward: bool = True,
463
+ reverse: bool = True,
464
+ include_hidden: bool = False,
465
+ seen_models: set[type[Any]] | None = None,
466
+ ) -> ImmutableList:
460
467
  """
461
468
  Internal helper function to return fields of the model.
462
469
  * If forward=True, then fields defined on this model are returned.
@@ -471,7 +478,7 @@ class Options:
471
478
  # We must keep track of which models we have already seen. Otherwise we
472
479
  # could include the same field multiple times from different models.
473
480
  topmost_call = seen_models is None
474
- if topmost_call:
481
+ if seen_models is None:
475
482
  seen_models = set()
476
483
  seen_models.add(self.model)
477
484
 
@@ -511,7 +518,7 @@ class Options:
511
518
  return fields
512
519
 
513
520
  @cached_property
514
- def total_unique_constraints(self):
521
+ def total_unique_constraints(self) -> list[UniqueConstraint]:
515
522
  """
516
523
  Return a list of total unique constraints. Useful for determining set
517
524
  of fields guaranteed to be unique for all rows.
@@ -527,7 +534,7 @@ class Options:
527
534
  ]
528
535
 
529
536
  @cached_property
530
- def _property_names(self):
537
+ def _property_names(self) -> frozenset[str]:
531
538
  """Return a set of the names of the properties defined on the model."""
532
539
  names = []
533
540
  for name in dir(self.model):
@@ -537,7 +544,7 @@ class Options:
537
544
  return frozenset(names)
538
545
 
539
546
  @cached_property
540
- def _non_pk_concrete_field_names(self):
547
+ def _non_pk_concrete_field_names(self) -> frozenset[str]:
541
548
  """
542
549
  Return a set of the non-primary key concrete field names defined on the model.
543
550
  """
@@ -550,7 +557,7 @@ class Options:
550
557
  return frozenset(names)
551
558
 
552
559
  @cached_property
553
- def db_returning_fields(self):
560
+ def db_returning_fields(self) -> list[Field]:
554
561
  """
555
562
  Private API intended only to be used by Plain itself.
556
563
  Fields to be returned after a database insert.
plain/models/otel.py CHANGED
@@ -1,10 +1,18 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  import traceback
5
+ from collections.abc import Generator
3
6
  from contextlib import contextmanager
4
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
5
8
 
6
9
  from opentelemetry import context as otel_context
7
10
  from opentelemetry import trace
11
+
12
+ if TYPE_CHECKING:
13
+ from opentelemetry.trace import Span
14
+
15
+ from plain.models.backends.base.base import BaseDatabaseWrapper
8
16
  from opentelemetry.semconv._incubating.attributes.db_attributes import (
9
17
  DB_QUERY_PARAMETER_TEMPLATE,
10
18
  DB_USER,
@@ -99,7 +107,9 @@ def _clean_identifier(identifier: str) -> str:
99
107
 
100
108
 
101
109
  @contextmanager
102
- def db_span(db, sql: Any, *, many: bool = False, params=None):
110
+ def db_span(
111
+ db: BaseDatabaseWrapper, sql: Any, *, many: bool = False, params: Any = None
112
+ ) -> Generator[Span | None, None, None]:
103
113
  """Open an OpenTelemetry CLIENT span for a database query.
104
114
 
105
115
  All common attributes (`db.*`, `network.*`, etc.) are set automatically.
@@ -107,7 +117,7 @@ def db_span(db, sql: Any, *, many: bool = False, params=None):
107
117
  """
108
118
 
109
119
  # Fast-exit if instrumentation suppression flag set in context.
110
- if otel_context.get_value(_SUPPRESS_KEY):
120
+ if otel_context.get_value(_SUPPRESS_KEY): # type: ignore[arg-type]
111
121
  yield None
112
122
  return
113
123
 
@@ -177,15 +187,15 @@ def db_span(db, sql: Any, *, many: bool = False, params=None):
177
187
 
178
188
 
179
189
  @contextmanager
180
- def suppress_db_tracing():
181
- token = otel_context.attach(otel_context.set_value(_SUPPRESS_KEY, True))
190
+ def suppress_db_tracing() -> Generator[None, None, None]:
191
+ token = otel_context.attach(otel_context.set_value(_SUPPRESS_KEY, True)) # type: ignore[arg-type]
182
192
  try:
183
193
  yield
184
194
  finally:
185
195
  otel_context.detach(token)
186
196
 
187
197
 
188
- def _get_code_attributes():
198
+ def _get_code_attributes() -> dict[str, Any]:
189
199
  """Extract code context attributes for the current database query.
190
200
 
191
201
  Returns a dict of OpenTelemetry code attributes.