plain.models 0.50.0__py3-none-any.whl → 0.51.1__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 (49) hide show
  1. plain/models/CHANGELOG.md +24 -0
  2. plain/models/README.md +26 -42
  3. plain/models/__init__.py +2 -0
  4. plain/models/backends/base/creation.py +2 -2
  5. plain/models/backends/base/introspection.py +8 -4
  6. plain/models/backends/base/schema.py +89 -71
  7. plain/models/backends/base/validation.py +1 -1
  8. plain/models/backends/mysql/compiler.py +1 -1
  9. plain/models/backends/mysql/operations.py +1 -1
  10. plain/models/backends/mysql/schema.py +4 -4
  11. plain/models/backends/postgresql/operations.py +1 -1
  12. plain/models/backends/postgresql/schema.py +3 -3
  13. plain/models/backends/sqlite3/operations.py +1 -1
  14. plain/models/backends/sqlite3/schema.py +61 -50
  15. plain/models/base.py +116 -163
  16. plain/models/cli.py +4 -4
  17. plain/models/constraints.py +14 -9
  18. plain/models/deletion.py +15 -14
  19. plain/models/expressions.py +21 -5
  20. plain/models/fields/__init__.py +20 -16
  21. plain/models/fields/json.py +3 -3
  22. plain/models/fields/related.py +73 -71
  23. plain/models/fields/related_descriptors.py +2 -2
  24. plain/models/fields/related_lookups.py +1 -1
  25. plain/models/fields/related_managers.py +21 -32
  26. plain/models/fields/reverse_related.py +8 -8
  27. plain/models/forms.py +12 -12
  28. plain/models/indexes.py +5 -4
  29. plain/models/meta.py +505 -0
  30. plain/models/migrations/operations/base.py +1 -1
  31. plain/models/migrations/operations/fields.py +6 -6
  32. plain/models/migrations/operations/models.py +18 -16
  33. plain/models/migrations/recorder.py +9 -5
  34. plain/models/migrations/state.py +35 -46
  35. plain/models/migrations/utils.py +1 -1
  36. plain/models/options.py +182 -518
  37. plain/models/preflight.py +7 -5
  38. plain/models/query.py +119 -65
  39. plain/models/query_utils.py +18 -13
  40. plain/models/registry.py +6 -5
  41. plain/models/sql/compiler.py +51 -37
  42. plain/models/sql/query.py +77 -68
  43. plain/models/sql/subqueries.py +4 -4
  44. plain/models/utils.py +4 -1
  45. {plain_models-0.50.0.dist-info → plain_models-0.51.1.dist-info}/METADATA +27 -43
  46. {plain_models-0.50.0.dist-info → plain_models-0.51.1.dist-info}/RECORD +49 -48
  47. {plain_models-0.50.0.dist-info → plain_models-0.51.1.dist-info}/WHEEL +0 -0
  48. {plain_models-0.50.0.dist-info → plain_models-0.51.1.dist-info}/entry_points.txt +0 -0
  49. {plain_models-0.50.0.dist-info → plain_models-0.51.1.dist-info}/licenses/LICENSE +0 -0
plain/models/cli.py CHANGED
@@ -118,19 +118,19 @@ def list_models(package_labels: tuple[str, ...], app_only: bool) -> None:
118
118
 
119
119
  for model in sorted(
120
120
  models_registry.get_models(),
121
- key=lambda m: (m._meta.package_label, m._meta.model_name),
121
+ key=lambda m: (m.model_options.package_label, m.model_options.model_name),
122
122
  ):
123
- pkg = model._meta.package_label
123
+ pkg = model.model_options.package_label
124
124
  pkg_name = packages_registry.get_package_config(pkg).name
125
125
  if app_only and not pkg_name.startswith("app"):
126
126
  continue
127
127
  if packages and pkg not in packages:
128
128
  continue
129
- fields = ", ".join(f.name for f in model._meta.get_fields())
129
+ fields = ", ".join(f.name for f in model._model_meta.get_fields())
130
130
  click.echo(
131
131
  f"{click.style(pkg, fg='cyan')}.{click.style(model.__name__, fg='blue')}"
132
132
  )
133
- click.echo(f" table: {model._meta.db_table}")
133
+ click.echo(f" table: {model.model_options.db_table}")
134
134
  click.echo(f" fields: {fields}")
135
135
  click.echo(f" package: {pkg_name}\n")
136
136
 
@@ -115,7 +115,7 @@ class CheckConstraint(BaseConstraint):
115
115
  def validate(
116
116
  self, model: Any, instance: Any, exclude: set[str] | None = None
117
117
  ) -> None:
118
- against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
118
+ against = instance._get_field_value_map(meta=model._model_meta, exclude=exclude)
119
119
  try:
120
120
  if not Q(self.check).check(against):
121
121
  raise ValidationError(
@@ -260,9 +260,10 @@ class UniqueConstraint(BaseConstraint):
260
260
  )
261
261
 
262
262
  def constraint_sql(self, model: Any, schema_editor: Any) -> str:
263
- fields = [model._meta.get_field(field_name) for field_name in self.fields]
263
+ fields = [model._model_meta.get_field(field_name) for field_name in self.fields]
264
264
  include = [
265
- model._meta.get_field(field_name).column for field_name in self.include
265
+ model._model_meta.get_field(field_name).column
266
+ for field_name in self.include
266
267
  ]
267
268
  condition = self._get_condition_sql(model, schema_editor)
268
269
  expressions = self._get_index_expressions(model, schema_editor)
@@ -278,9 +279,10 @@ class UniqueConstraint(BaseConstraint):
278
279
  )
279
280
 
280
281
  def create_sql(self, model: Any, schema_editor: Any) -> str:
281
- fields = [model._meta.get_field(field_name) for field_name in self.fields]
282
+ fields = [model._model_meta.get_field(field_name) for field_name in self.fields]
282
283
  include = [
283
- model._meta.get_field(field_name).column for field_name in self.include
284
+ model._model_meta.get_field(field_name).column
285
+ for field_name in self.include
284
286
  ]
285
287
  condition = self._get_condition_sql(model, schema_editor)
286
288
  expressions = self._get_index_expressions(model, schema_editor)
@@ -298,7 +300,8 @@ class UniqueConstraint(BaseConstraint):
298
300
  def remove_sql(self, model: Any, schema_editor: Any) -> str:
299
301
  condition = self._get_condition_sql(model, schema_editor)
300
302
  include = [
301
- model._meta.get_field(field_name).column for field_name in self.include
303
+ model._model_meta.get_field(field_name).column
304
+ for field_name in self.include
302
305
  ]
303
306
  expressions = self._get_index_expressions(model, schema_editor)
304
307
  return schema_editor._delete_unique_sql(
@@ -372,7 +375,7 @@ class UniqueConstraint(BaseConstraint):
372
375
  for field_name in self.fields:
373
376
  if exclude and field_name in exclude:
374
377
  return
375
- field = model._meta.get_field(field_name)
378
+ field = model._model_meta.get_field(field_name)
376
379
  lookup_value = getattr(instance, field.attname)
377
380
  if lookup_value is None:
378
381
  # A composite constraint containing NULL value cannot cause
@@ -393,7 +396,7 @@ class UniqueConstraint(BaseConstraint):
393
396
  replacements = {
394
397
  F(field): value
395
398
  for field, value in instance._get_field_value_map(
396
- meta=model._meta, exclude=exclude
399
+ meta=model._model_meta, exclude=exclude
397
400
  ).items()
398
401
  }
399
402
  expressions = []
@@ -422,7 +425,9 @@ class UniqueConstraint(BaseConstraint):
422
425
  instance.unique_error_message(model, self.fields),
423
426
  )
424
427
  else:
425
- against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
428
+ against = instance._get_field_value_map(
429
+ meta=model._model_meta, exclude=exclude
430
+ )
426
431
  try:
427
432
  if (self.condition & Exists(queryset.filter(self.condition))).check(
428
433
  against
plain/models/deletion.py CHANGED
@@ -13,6 +13,7 @@ from plain.models import (
13
13
  transaction,
14
14
  )
15
15
  from plain.models.db import IntegrityError, db_connection
16
+ from plain.models.meta import Meta
16
17
  from plain.models.query import QuerySet
17
18
 
18
19
  if TYPE_CHECKING:
@@ -89,12 +90,12 @@ def DO_NOTHING(collector: Collector, field: Field, sub_objs: Any) -> None:
89
90
  pass
90
91
 
91
92
 
92
- def get_candidate_relations_to_delete(opts: Any) -> Generator[Any, None, None]:
93
+ def get_candidate_relations_to_delete(meta: Meta) -> Generator[Any, None, None]:
93
94
  # The candidate relations are the ones that come from N-1 and 1-1 relations.
94
95
  # N-N (i.e., many-to-many) relations aren't candidates for deletion.
95
96
  return (
96
97
  f
97
- for f in opts.get_fields(include_hidden=True)
98
+ for f in meta.get_fields(include_hidden=True)
98
99
  if f.auto_created and not f.concrete and f.one_to_many
99
100
  )
100
101
 
@@ -208,8 +209,8 @@ class Collector:
208
209
  """
209
210
  if from_field and from_field.remote_field.on_delete is not CASCADE:
210
211
  return False
211
- if hasattr(objs, "_meta"):
212
- model = objs._meta.model
212
+ if hasattr(objs, "_model_meta"):
213
+ model = objs._model_meta.model
213
214
  elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"):
214
215
  model = objs.model
215
216
  else:
@@ -217,12 +218,12 @@ class Collector:
217
218
 
218
219
  # The use of from_field comes from the need to avoid cascade back to
219
220
  # parent when parent delete is cascading to child.
220
- opts = model._meta
221
+ meta = model._model_meta
221
222
  return (
222
223
  # Foreign keys pointing to this model.
223
224
  all(
224
225
  related.field.remote_field.on_delete is DO_NOTHING
225
- for related in get_candidate_relations_to_delete(opts)
226
+ for related in get_candidate_relations_to_delete(meta)
226
227
  )
227
228
  )
228
229
 
@@ -289,7 +290,7 @@ class Collector:
289
290
 
290
291
  model_fast_deletes = defaultdict(list)
291
292
  protected_objects = defaultdict(list)
292
- for related in get_candidate_relations_to_delete(model._meta):
293
+ for related in get_candidate_relations_to_delete(model._model_meta):
293
294
  field = related.field
294
295
  on_delete = field.remote_field.on_delete
295
296
  if on_delete == DO_NOTHING:
@@ -312,7 +313,7 @@ class Collector:
312
313
  chain.from_iterable(
313
314
  (rf.attname for rf in rel.field.foreign_related_fields)
314
315
  for rel in get_candidate_relations_to_delete(
315
- related_model._meta
316
+ related_model._model_meta
316
317
  )
317
318
  )
318
319
  )
@@ -373,7 +374,7 @@ class Collector:
373
374
  [(f"{related_field.name}__in", objs) for related_field in related_fields],
374
375
  connector=query_utils.Q.OR,
375
376
  )
376
- return related_model._meta.base_queryset.filter(predicate)
377
+ return related_model._model_meta.base_queryset.filter(predicate)
377
378
 
378
379
  def sort(self) -> None:
379
380
  sorted_models = []
@@ -411,15 +412,15 @@ class Collector:
411
412
  if self.can_fast_delete(instance):
412
413
  with transaction.mark_for_rollback_on_error():
413
414
  count = sql.DeleteQuery(model).delete_batch([instance.id])
414
- setattr(instance, model._meta.get_field("id").attname, None)
415
- 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}
416
417
 
417
418
  with transaction.atomic(savepoint=False):
418
419
  # fast deletes
419
420
  for qs in self.fast_deletes:
420
421
  count = qs._raw_delete()
421
422
  if count:
422
- deleted_counter[qs.model._meta.label] += count
423
+ deleted_counter[qs.model.model_options.label] += count
423
424
 
424
425
  # update fields
425
426
  for (field, value), instances_list in self.field_updates.items():
@@ -453,9 +454,9 @@ class Collector:
453
454
  id_list = [obj.id for obj in instances]
454
455
  count = query.delete_batch(id_list)
455
456
  if count:
456
- deleted_counter[model._meta.label] += count
457
+ deleted_counter[model.model_options.label] += count
457
458
 
458
459
  for model, instances in self.data.items():
459
460
  for instance in instances:
460
- setattr(instance, model._meta.get_field("id").attname, None)
461
+ setattr(instance, model._model_meta.get_field("id").attname, None)
461
462
  return sum(deleted_counter.values()), dict(deleted_counter)
@@ -28,7 +28,9 @@ if TYPE_CHECKING:
28
28
 
29
29
  from plain.models.backends.base.base import BaseDatabaseWrapper
30
30
  from plain.models.fields import Field
31
+ from plain.models.query import QuerySet
31
32
  from plain.models.sql.compiler import SQLCompiler
33
+ from plain.models.sql.query import Query
32
34
 
33
35
 
34
36
  class SQLiteNumericMixin:
@@ -518,7 +520,7 @@ class Expression(BaseExpression, Combinable):
518
520
  for arg, value in arguments:
519
521
  if isinstance(value, fields.Field):
520
522
  if value.name and value.model:
521
- value = (value.model._meta.label, value.name)
523
+ value = (value.model.model_options.label, value.name)
522
524
  else:
523
525
  value = type(value)
524
526
  else:
@@ -1635,9 +1637,23 @@ class Subquery(BaseExpression, Combinable):
1635
1637
  contains_aggregate = False
1636
1638
  empty_result_set_value = None
1637
1639
 
1638
- def __init__(self, queryset: Any, output_field: Field | None = None, **extra: Any):
1640
+ def __init__(
1641
+ self,
1642
+ query: QuerySet[Any] | Query,
1643
+ output_field: Field | None = None,
1644
+ **extra: Any,
1645
+ ):
1646
+ # Import here to avoid circular import
1647
+ from plain.models.sql.query import Query
1648
+
1639
1649
  # Allow the usage of both QuerySet and sql.Query objects.
1640
- self.query = getattr(queryset, "query", queryset).clone()
1650
+ if isinstance(query, Query):
1651
+ # It's already a Query object, use it directly
1652
+ sql_query = query
1653
+ else:
1654
+ # It's a QuerySet, extract the sql.Query
1655
+ sql_query = query.sql_query
1656
+ self.query = sql_query.clone()
1641
1657
  self.query.subquery = True
1642
1658
  self.extra = extra
1643
1659
  super().__init__(output_field)
@@ -1688,8 +1704,8 @@ class Exists(Subquery):
1688
1704
  output_field = fields.BooleanField()
1689
1705
  empty_result_set_value = False
1690
1706
 
1691
- def __init__(self, queryset: Any, **kwargs: Any):
1692
- super().__init__(queryset, **kwargs)
1707
+ def __init__(self, query: QuerySet[Any] | Query, **kwargs: Any):
1708
+ super().__init__(query, **kwargs)
1693
1709
  self.query = self.query.exists()
1694
1710
 
1695
1711
  def select_format(
@@ -36,6 +36,7 @@ from ..registry import models_registry
36
36
 
37
37
  if TYPE_CHECKING:
38
38
  from plain.models.backends.base.base import BaseDatabaseWrapper
39
+ from plain.models.fields.reverse_related import ForeignObjectRel
39
40
  from plain.models.sql.compiler import SQLCompiler
40
41
 
41
42
  __all__ = [
@@ -81,7 +82,7 @@ BLANK_CHOICE_DASH = [("", "---------")]
81
82
 
82
83
 
83
84
  def _load_field(package_label: str, model_name: str, field_name: str) -> Field:
84
- return models_registry.get_model(package_label, model_name)._meta.get_field(
85
+ return models_registry.get_model(package_label, model_name)._model_meta.get_field(
85
86
  field_name
86
87
  )
87
88
 
@@ -169,7 +170,7 @@ class Field(RegisterLookupMixin):
169
170
  max_length: int | None = None,
170
171
  required: bool = True,
171
172
  allow_null: bool = False,
172
- rel: Any = None,
173
+ rel: ForeignObjectRel | None = None,
173
174
  default: Any = NOT_PROVIDED,
174
175
  choices: Any = None,
175
176
  db_column: str | None = None,
@@ -212,7 +213,7 @@ class Field(RegisterLookupMixin):
212
213
  if not hasattr(self, "model"):
213
214
  return super().__str__()
214
215
  model = self.model
215
- return f"{model._meta.label}.{self.name}"
216
+ return f"{model.model_options.label}.{self.name}"
216
217
 
217
218
  def __repr__(self) -> str:
218
219
  """Display the module, class, and name of the field."""
@@ -346,7 +347,7 @@ class Field(RegisterLookupMixin):
346
347
  errors = []
347
348
  if not (
348
349
  db_connection.features.supports_comments
349
- or "supports_comments" in self.model._meta.required_db_features
350
+ or "supports_comments" in self.model.model_options.required_db_features
350
351
  ):
351
352
  errors.append(
352
353
  PreflightResult(
@@ -399,7 +400,7 @@ class Field(RegisterLookupMixin):
399
400
  return errors
400
401
 
401
402
  def get_col(self, alias: str, output_field: Field | None = None) -> Any:
402
- if alias == self.model._meta.db_table and (
403
+ if alias == self.model.model_options.db_table and (
403
404
  output_field is None or output_field == self
404
405
  ):
405
406
  return self.cached_col
@@ -411,7 +412,7 @@ class Field(RegisterLookupMixin):
411
412
  def cached_col(self) -> Any:
412
413
  from plain.models.expressions import Col
413
414
 
414
- return Col(self.model._meta.db_table, self)
415
+ return Col(self.model.model_options.db_table, self)
415
416
 
416
417
  def select_format(
417
418
  self, compiler: SQLCompiler, sql: str, params: Any
@@ -528,9 +529,12 @@ class Field(RegisterLookupMixin):
528
529
  return not hasattr(self, "model") # Order no-model fields first
529
530
  else:
530
531
  # creation_counter's are equal, compare only models.
531
- return (self.model._meta.package_label, self.model._meta.model_name) < (
532
- other.model._meta.package_label,
533
- other.model._meta.model_name,
532
+ return (
533
+ self.model.model_options.package_label,
534
+ self.model.model_options.model_name,
535
+ ) < (
536
+ other.model.model_options.package_label,
537
+ other.model.model_options.model_name,
534
538
  )
535
539
  return NotImplemented
536
540
 
@@ -563,7 +567,7 @@ class Field(RegisterLookupMixin):
563
567
  | tuple[Callable[..., Field], tuple[str, str, str]]
564
568
  ):
565
569
  """
566
- Pickling should return the model._meta.fields instance of the field,
570
+ Pickling should return the model._model_meta.fields instance of the field,
567
571
  not a new copy of that field. So, use the app registry to load the
568
572
  model and then the field back.
569
573
  """
@@ -579,8 +583,8 @@ class Field(RegisterLookupMixin):
579
583
  state.pop("_get_default", None)
580
584
  return _empty, (self.__class__,), state
581
585
  return _load_field, (
582
- self.model._meta.package_label,
583
- self.model._meta.object_name,
586
+ self.model.model_options.package_label,
587
+ self.model.model_options.object_name,
584
588
  self.name,
585
589
  )
586
590
 
@@ -782,7 +786,7 @@ class Field(RegisterLookupMixin):
782
786
  """
783
787
  self.set_attributes_from_name(name)
784
788
  self.model = cls
785
- cls._meta.add_field(self)
789
+ cls._model_meta.add_field(self)
786
790
  if self.column:
787
791
  setattr(cls, self.attname, self.descriptor_class(self))
788
792
 
@@ -967,7 +971,7 @@ class CharField(Field):
967
971
  if (
968
972
  db_connection.features.supports_unlimited_charfield
969
973
  or "supports_unlimited_charfield"
970
- in self.model._meta.required_db_features
974
+ in self.model.model_options.required_db_features
971
975
  ):
972
976
  return []
973
977
  return [
@@ -997,7 +1001,7 @@ class CharField(Field):
997
1001
  if not (
998
1002
  self.db_collation is None
999
1003
  or "supports_collation_on_charfield"
1000
- in self.model._meta.required_db_features
1004
+ in self.model.model_options.required_db_features
1001
1005
  or db_connection.features.supports_collation_on_charfield
1002
1006
  ):
1003
1007
  errors.append(
@@ -1863,7 +1867,7 @@ class TextField(Field):
1863
1867
  if not (
1864
1868
  self.db_collation is None
1865
1869
  or "supports_collation_on_textfield"
1866
- in self.model._meta.required_db_features
1870
+ in self.model.model_options.required_db_features
1867
1871
  or db_connection.features.supports_collation_on_textfield
1868
1872
  ):
1869
1873
  errors.append(
@@ -59,13 +59,13 @@ class JSONField(CheckFieldDefaultMixin, Field):
59
59
  errors = []
60
60
 
61
61
  if (
62
- self.model._meta.required_db_vendor
63
- and self.model._meta.required_db_vendor != db_connection.vendor
62
+ self.model.model_options.required_db_vendor
63
+ and self.model.model_options.required_db_vendor != db_connection.vendor
64
64
  ):
65
65
  return errors
66
66
 
67
67
  if not (
68
- "supports_json_field" in self.model._meta.required_db_features
68
+ "supports_json_field" in self.model.model_options.required_db_features
69
69
  or db_connection.features.supports_json_field
70
70
  ):
71
71
  errors.append(