plain.models 0.50.0__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.
- plain/models/CHANGELOG.md +14 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -0
- plain/models/backends/base/creation.py +2 -2
- plain/models/backends/base/introspection.py +8 -4
- plain/models/backends/base/schema.py +89 -71
- plain/models/backends/base/validation.py +1 -1
- plain/models/backends/mysql/compiler.py +1 -1
- plain/models/backends/mysql/operations.py +1 -1
- plain/models/backends/mysql/schema.py +4 -4
- plain/models/backends/postgresql/operations.py +1 -1
- plain/models/backends/postgresql/schema.py +3 -3
- plain/models/backends/sqlite3/operations.py +1 -1
- plain/models/backends/sqlite3/schema.py +61 -50
- plain/models/base.py +116 -163
- plain/models/cli.py +4 -4
- plain/models/constraints.py +14 -9
- plain/models/deletion.py +15 -14
- plain/models/expressions.py +1 -1
- plain/models/fields/__init__.py +20 -16
- plain/models/fields/json.py +3 -3
- plain/models/fields/related.py +73 -71
- plain/models/fields/related_descriptors.py +2 -2
- plain/models/fields/related_lookups.py +1 -1
- plain/models/fields/related_managers.py +21 -32
- plain/models/fields/reverse_related.py +8 -8
- plain/models/forms.py +12 -12
- plain/models/indexes.py +5 -4
- plain/models/meta.py +505 -0
- plain/models/migrations/operations/base.py +1 -1
- plain/models/migrations/operations/fields.py +6 -6
- plain/models/migrations/operations/models.py +18 -16
- plain/models/migrations/recorder.py +9 -5
- plain/models/migrations/state.py +35 -46
- plain/models/migrations/utils.py +1 -1
- plain/models/options.py +182 -518
- plain/models/preflight.py +7 -5
- plain/models/query.py +119 -65
- plain/models/query_utils.py +18 -13
- plain/models/registry.py +6 -5
- plain/models/sql/compiler.py +51 -37
- plain/models/sql/query.py +77 -68
- plain/models/sql/subqueries.py +4 -4
- plain/models/utils.py +4 -1
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/RECORD +49 -48
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.50.0.dist-info → plain_models-0.51.0.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.
|
121
|
+
key=lambda m: (m.model_options.package_label, m.model_options.model_name),
|
122
122
|
):
|
123
|
-
pkg = model.
|
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.
|
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.
|
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
|
|
plain/models/constraints.py
CHANGED
@@ -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.
|
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.
|
263
|
+
fields = [model._model_meta.get_field(field_name) for field_name in self.fields]
|
264
264
|
include = [
|
265
|
-
model.
|
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.
|
282
|
+
fields = [model._model_meta.get_field(field_name) for field_name in self.fields]
|
282
283
|
include = [
|
283
|
-
model.
|
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.
|
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.
|
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.
|
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(
|
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(
|
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
|
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, "
|
212
|
-
model = objs.
|
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
|
-
|
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(
|
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.
|
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.
|
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.
|
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.
|
415
|
-
return count, {model.
|
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.
|
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.
|
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.
|
461
|
+
setattr(instance, model._model_meta.get_field("id").attname, None)
|
461
462
|
return sum(deleted_counter.values()), dict(deleted_counter)
|
plain/models/expressions.py
CHANGED
@@ -518,7 +518,7 @@ class Expression(BaseExpression, Combinable):
|
|
518
518
|
for arg, value in arguments:
|
519
519
|
if isinstance(value, fields.Field):
|
520
520
|
if value.name and value.model:
|
521
|
-
value = (value.model.
|
521
|
+
value = (value.model.model_options.label, value.name)
|
522
522
|
else:
|
523
523
|
value = type(value)
|
524
524
|
else:
|
plain/models/fields/__init__.py
CHANGED
@@ -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).
|
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:
|
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.
|
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.
|
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.
|
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.
|
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 (
|
532
|
-
|
533
|
-
|
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.
|
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.
|
583
|
-
self.model.
|
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.
|
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.
|
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.
|
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.
|
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(
|
plain/models/fields/json.py
CHANGED
@@ -59,13 +59,13 @@ class JSONField(CheckFieldDefaultMixin, Field):
|
|
59
59
|
errors = []
|
60
60
|
|
61
61
|
if (
|
62
|
-
self.model.
|
63
|
-
and self.model.
|
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.
|
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(
|