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/options.py
CHANGED
@@ -1,528 +1,207 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
import inspect
|
5
|
-
from collections import defaultdict
|
6
|
-
from functools import cached_property
|
3
|
+
from collections.abc import Sequence
|
7
4
|
from typing import TYPE_CHECKING, Any
|
8
5
|
|
9
|
-
from plain.models import
|
10
|
-
from plain.models.constraints import UniqueConstraint
|
6
|
+
from plain.models.backends.utils import truncate_name
|
11
7
|
from plain.models.db import db_connection
|
12
|
-
from plain.
|
13
|
-
from plain.models.query import QuerySet
|
14
|
-
from plain.utils.datastructures import ImmutableList
|
8
|
+
from plain.packages import packages_registry
|
15
9
|
|
16
10
|
if TYPE_CHECKING:
|
17
|
-
from plain.models.
|
18
|
-
from plain.models.
|
11
|
+
from plain.models.base import Model
|
12
|
+
from plain.models.constraints import BaseConstraint
|
13
|
+
from plain.models.indexes import Index
|
19
14
|
|
20
|
-
PROXY_PARENTS = object()
|
21
15
|
|
22
|
-
|
16
|
+
class Options:
|
17
|
+
"""
|
18
|
+
Model options descriptor and container.
|
19
|
+
|
20
|
+
Acts as both a descriptor (for lazy initialization and access control)
|
21
|
+
and the actual options instance (cached per model class).
|
22
|
+
"""
|
23
|
+
|
24
|
+
# Type annotations for attributes set in _create_and_cache
|
25
|
+
# These exist on cached instances, not on the descriptor itself
|
26
|
+
model: type[Model]
|
27
|
+
package_label: str
|
28
|
+
db_table: str
|
29
|
+
db_table_comment: str
|
30
|
+
ordering: Sequence[str]
|
31
|
+
indexes: Sequence[Index]
|
32
|
+
constraints: Sequence[BaseConstraint]
|
33
|
+
required_db_features: Sequence[str]
|
34
|
+
required_db_vendor: str | None
|
35
|
+
_provided_options: set[str]
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
*,
|
40
|
+
db_table: str | None = None,
|
41
|
+
db_table_comment: str | None = None,
|
42
|
+
ordering: Sequence[str] | None = None,
|
43
|
+
indexes: Sequence[Index] | None = None,
|
44
|
+
constraints: Sequence[BaseConstraint] | None = None,
|
45
|
+
required_db_features: Sequence[str] | None = None,
|
46
|
+
required_db_vendor: str | None = None,
|
47
|
+
package_label: str | None = None,
|
48
|
+
):
|
49
|
+
"""
|
50
|
+
Initialize the descriptor with optional configuration.
|
51
|
+
|
52
|
+
This is called ONCE when defining the base Model class, or when
|
53
|
+
a user explicitly sets model_options = Options(...) on their model.
|
54
|
+
The descriptor then creates cached instances per model subclass.
|
55
|
+
"""
|
56
|
+
self._config = {
|
57
|
+
"db_table": db_table,
|
58
|
+
"db_table_comment": db_table_comment,
|
59
|
+
"ordering": ordering,
|
60
|
+
"indexes": indexes,
|
61
|
+
"constraints": constraints,
|
62
|
+
"required_db_features": required_db_features,
|
63
|
+
"required_db_vendor": required_db_vendor,
|
64
|
+
"package_label": package_label,
|
65
|
+
}
|
66
|
+
self._cache: dict[type[Model], Options] = {}
|
67
|
+
|
68
|
+
def __get__(self, instance: Any, owner: type[Model]) -> Options:
|
69
|
+
"""
|
70
|
+
Descriptor protocol - returns cached Options for the model class.
|
71
|
+
|
72
|
+
This is called when accessing Model.model_options and returns a per-class
|
73
|
+
cached instance created by _create_and_cache().
|
74
|
+
|
75
|
+
Can be accessed from both class and instances:
|
76
|
+
- MyModel.model_options (class access)
|
77
|
+
- my_instance.model_options (instance access - returns class's options)
|
78
|
+
"""
|
79
|
+
# Allow instance access - just return the class's options
|
80
|
+
if instance is not None:
|
81
|
+
owner = instance.__class__
|
82
|
+
|
83
|
+
# Skip for the base Model class - return descriptor
|
84
|
+
if owner.__name__ == "Model" and owner.__module__ == "plain.models.base":
|
85
|
+
return self # type: ignore
|
86
|
+
|
87
|
+
# Return cached instance or create new one
|
88
|
+
if owner not in self._cache:
|
89
|
+
return self._create_and_cache(owner)
|
90
|
+
|
91
|
+
return self._cache[owner]
|
92
|
+
|
93
|
+
def _create_and_cache(self, model: type[Model]) -> Options:
|
94
|
+
"""Create Options and cache it."""
|
95
|
+
# Create instance without calling __init__
|
96
|
+
instance = Options.__new__(Options)
|
97
|
+
|
98
|
+
# Track which options were explicitly provided by user
|
99
|
+
# Note: package_label is excluded because it's passed separately in migrations
|
100
|
+
instance._provided_options = {
|
101
|
+
k for k, v in self._config.items() if v is not None and k != "package_label"
|
102
|
+
}
|
103
|
+
|
104
|
+
instance.model = model
|
105
|
+
|
106
|
+
# Resolve package_label
|
107
|
+
package_label = self._config.get("package_label")
|
108
|
+
if package_label is None:
|
109
|
+
module = model.__module__
|
110
|
+
package_config = packages_registry.get_containing_package_config(module)
|
111
|
+
if package_config is None:
|
112
|
+
raise RuntimeError(
|
113
|
+
f"Model class {module}.{model.__name__} doesn't declare an explicit "
|
114
|
+
"package_label and isn't in an application in INSTALLED_PACKAGES."
|
115
|
+
)
|
116
|
+
instance.package_label = package_config.package_label
|
117
|
+
else:
|
118
|
+
instance.package_label = package_label
|
23
119
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
120
|
+
# Set db_table
|
121
|
+
db_table = self._config.get("db_table")
|
122
|
+
if db_table is None:
|
123
|
+
instance.db_table = truncate_name(
|
124
|
+
f"{instance.package_label}_{model.__name__.lower()}",
|
125
|
+
db_connection.ops.max_name_length(),
|
126
|
+
)
|
127
|
+
else:
|
128
|
+
instance.db_table = db_table
|
28
129
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
"models_registry",
|
36
|
-
"required_db_features",
|
37
|
-
"required_db_vendor",
|
38
|
-
"indexes",
|
39
|
-
"constraints",
|
40
|
-
)
|
130
|
+
instance.db_table_comment = self._config.get("db_table_comment") or ""
|
131
|
+
instance.ordering = self._config.get("ordering") or []
|
132
|
+
instance.indexes = self._config.get("indexes") or []
|
133
|
+
instance.constraints = self._config.get("constraints") or []
|
134
|
+
instance.required_db_features = self._config.get("required_db_features") or []
|
135
|
+
instance.required_db_vendor = self._config.get("required_db_vendor")
|
41
136
|
|
137
|
+
# Format names with class interpolation
|
138
|
+
instance.constraints = instance._format_names_with_class(instance.constraints)
|
139
|
+
instance.indexes = instance._format_names_with_class(instance.indexes)
|
42
140
|
|
43
|
-
|
44
|
-
|
141
|
+
# Cache early to prevent recursion if needed
|
142
|
+
self._cache[model] = instance
|
45
143
|
|
144
|
+
return instance
|
46
145
|
|
47
|
-
|
48
|
-
|
49
|
-
"
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
"
|
55
|
-
|
56
|
-
"queryset",
|
57
|
-
}
|
58
|
-
REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
|
59
|
-
|
60
|
-
default_models_registry = models_registry
|
61
|
-
|
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
|
78
|
-
|
79
|
-
# List of all lookups defined in ForeignKey 'limit_choices_to' options
|
80
|
-
# from *other* models. Needed for some admin checks. Internal use only.
|
81
|
-
self.related_fkey_lookups: list[Any] = []
|
82
|
-
|
83
|
-
# A custom app registry to use, if you're making a separate model set.
|
84
|
-
self.models_registry: Any = self.default_models_registry
|
146
|
+
@property
|
147
|
+
def object_name(self) -> str:
|
148
|
+
"""The model class name."""
|
149
|
+
return self.model.__name__
|
150
|
+
|
151
|
+
@property
|
152
|
+
def model_name(self) -> str:
|
153
|
+
"""The model class name in lowercase."""
|
154
|
+
return self.object_name.lower()
|
85
155
|
|
86
156
|
@property
|
87
157
|
def label(self) -> str:
|
158
|
+
"""The model label: package_label.ClassName"""
|
88
159
|
return f"{self.package_label}.{self.object_name}"
|
89
160
|
|
90
161
|
@property
|
91
162
|
def label_lower(self) -> str:
|
163
|
+
"""The model label in lowercase: package_label.classname"""
|
92
164
|
return f"{self.package_label}.{self.model_name}"
|
93
165
|
|
94
|
-
def
|
95
|
-
from plain.models.backends.utils import truncate_name
|
96
|
-
|
97
|
-
cls._meta = self
|
98
|
-
self.model: type[Any] = cls
|
99
|
-
# First, construct the default values for these options.
|
100
|
-
self.object_name = cls.__name__
|
101
|
-
self.model_name = self.object_name.lower()
|
102
|
-
|
103
|
-
# Store the original user-defined values for each option,
|
104
|
-
# for use when serializing the model definition
|
105
|
-
self.original_attrs = {}
|
106
|
-
|
107
|
-
# Next, apply any overridden values from 'class Meta'.
|
108
|
-
if self.meta:
|
109
|
-
meta_attrs = self.meta.__dict__.copy()
|
110
|
-
for name in self.meta.__dict__:
|
111
|
-
# Ignore any private attributes that Plain doesn't care about.
|
112
|
-
# NOTE: We can't modify a dictionary's contents while looping
|
113
|
-
# over it, so we loop over the *original* dictionary instead.
|
114
|
-
if name.startswith("_"):
|
115
|
-
del meta_attrs[name]
|
116
|
-
for attr_name in DEFAULT_NAMES:
|
117
|
-
if attr_name in meta_attrs:
|
118
|
-
setattr(self, attr_name, meta_attrs.pop(attr_name))
|
119
|
-
self.original_attrs[attr_name] = getattr(self, attr_name)
|
120
|
-
elif hasattr(self.meta, attr_name):
|
121
|
-
setattr(self, attr_name, getattr(self.meta, attr_name))
|
122
|
-
self.original_attrs[attr_name] = getattr(self, attr_name)
|
123
|
-
|
124
|
-
# Package label/class name interpolation for names of constraints and
|
125
|
-
# indexes.
|
126
|
-
for attr_name in {"constraints", "indexes"}:
|
127
|
-
objs = getattr(self, attr_name, [])
|
128
|
-
setattr(self, attr_name, self._format_names_with_class(cls, objs))
|
129
|
-
|
130
|
-
# Any leftover attributes must be invalid.
|
131
|
-
if meta_attrs != {}:
|
132
|
-
raise TypeError(
|
133
|
-
"'class Meta' got invalid attribute(s): {}".format(
|
134
|
-
",".join(meta_attrs)
|
135
|
-
)
|
136
|
-
)
|
137
|
-
|
138
|
-
del self.meta
|
139
|
-
|
140
|
-
# If the db_table wasn't provided, use the package_label + model_name.
|
141
|
-
if not self.db_table:
|
142
|
-
self.db_table = f"{self.package_label}_{self.model_name}"
|
143
|
-
self.db_table = truncate_name(
|
144
|
-
self.db_table,
|
145
|
-
db_connection.ops.max_name_length(),
|
146
|
-
)
|
147
|
-
|
148
|
-
def _format_names_with_class(self, cls: type[Any], objs: list[Any]) -> list[Any]:
|
166
|
+
def _format_names_with_class(self, objs: list[Any]) -> list[Any]:
|
149
167
|
"""Package label/class name interpolation for object names."""
|
150
168
|
new_objs = []
|
151
169
|
for obj in objs:
|
152
170
|
obj = obj.clone()
|
153
171
|
obj.name = obj.name % {
|
154
|
-
"package_label":
|
155
|
-
"class":
|
172
|
+
"package_label": self.package_label.lower(),
|
173
|
+
"class": self.model.__name__.lower(),
|
156
174
|
}
|
157
175
|
new_objs.append(obj)
|
158
176
|
return new_objs
|
159
177
|
|
160
|
-
def
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
if (
|
179
|
-
field.is_relation
|
180
|
-
and hasattr(field.remote_field, "model")
|
181
|
-
and field.remote_field.model
|
182
|
-
):
|
183
|
-
try:
|
184
|
-
field.remote_field.model._meta._expire_cache(forward=False)
|
185
|
-
except AttributeError:
|
186
|
-
pass
|
187
|
-
self._expire_cache()
|
188
|
-
else:
|
189
|
-
self._expire_cache(reverse=False)
|
190
|
-
|
191
|
-
def __repr__(self) -> str:
|
192
|
-
return f"<Options for {self.object_name}>"
|
193
|
-
|
194
|
-
def __str__(self) -> str:
|
195
|
-
return self.label_lower
|
196
|
-
|
197
|
-
def can_migrate(self, connection: BaseDatabaseWrapper) -> bool:
|
198
|
-
"""
|
199
|
-
Return True if the model can/should be migrated on the given
|
200
|
-
`connection` object.
|
201
|
-
"""
|
202
|
-
if self.required_db_vendor:
|
203
|
-
return self.required_db_vendor == connection.vendor
|
204
|
-
if self.required_db_features:
|
205
|
-
return all(
|
206
|
-
getattr(connection.features, feat, False)
|
207
|
-
for feat in self.required_db_features
|
208
|
-
)
|
209
|
-
return True
|
210
|
-
|
211
|
-
@property
|
212
|
-
def base_queryset(self) -> QuerySet:
|
213
|
-
"""
|
214
|
-
The base queryset is used by Plain's internal operations like cascading
|
215
|
-
deletes, migrations, and related object lookups. It provides access to
|
216
|
-
all objects in the database without any filtering, ensuring Plain can
|
217
|
-
always see the complete dataset when performing framework operations.
|
218
|
-
|
219
|
-
Unlike user-defined querysets which may filter results (e.g. only active
|
220
|
-
objects), the base queryset must never filter out rows to prevent
|
221
|
-
incomplete results in related queries.
|
222
|
-
"""
|
223
|
-
return QuerySet(model=self.model)
|
178
|
+
def export_for_migrations(self) -> dict[str, Any]:
|
179
|
+
"""Export user-provided options for migrations."""
|
180
|
+
options = {}
|
181
|
+
for name in self._provided_options:
|
182
|
+
if name == "indexes":
|
183
|
+
# Clone indexes and ensure names are set
|
184
|
+
indexes = [idx.clone() for idx in self.indexes]
|
185
|
+
for index in indexes:
|
186
|
+
if not index.name:
|
187
|
+
index.set_name_with_model(self.model)
|
188
|
+
options["indexes"] = indexes
|
189
|
+
elif name == "constraints":
|
190
|
+
# Clone constraints
|
191
|
+
options["constraints"] = [con.clone() for con in self.constraints]
|
192
|
+
else:
|
193
|
+
# Use current attribute value
|
194
|
+
options[name] = getattr(self, name)
|
195
|
+
return options
|
224
196
|
|
225
197
|
@property
|
226
|
-
def
|
227
|
-
if self.queryset_class:
|
228
|
-
return self.queryset_class(model=self.model)
|
229
|
-
return QuerySet(model=self.model)
|
230
|
-
|
231
|
-
@cached_property
|
232
|
-
def fields(self) -> ImmutableList:
|
233
|
-
"""
|
234
|
-
Return a list of all forward fields on the model and its parents,
|
235
|
-
excluding ManyToManyFields.
|
236
|
-
|
237
|
-
Private API intended only to be used by Plain itself; get_fields()
|
238
|
-
combined with filtering of field properties is the public API for
|
239
|
-
obtaining this field list.
|
240
|
-
"""
|
241
|
-
|
242
|
-
# For legacy reasons, the fields property should only contain forward
|
243
|
-
# fields that are not private or with a m2m cardinality. Therefore we
|
244
|
-
# pass these three filters as filters to the generator.
|
245
|
-
# The third lambda is a longwinded way of checking f.related_model - we don't
|
246
|
-
# use that property directly because related_model is a cached property,
|
247
|
-
# and all the models may not have been loaded yet; we don't want to cache
|
248
|
-
# the string reference to the related_model.
|
249
|
-
def is_not_an_m2m_field(f: Any) -> bool:
|
250
|
-
return not (f.is_relation and f.many_to_many)
|
251
|
-
|
252
|
-
def is_not_a_generic_relation(f: Any) -> bool:
|
253
|
-
return not (f.is_relation and f.one_to_many)
|
254
|
-
|
255
|
-
def is_not_a_generic_foreign_key(f: Any) -> bool:
|
256
|
-
return not (
|
257
|
-
f.is_relation
|
258
|
-
and f.many_to_one
|
259
|
-
and not (hasattr(f.remote_field, "model") and f.remote_field.model)
|
260
|
-
)
|
261
|
-
|
262
|
-
return make_immutable_fields_list(
|
263
|
-
"fields",
|
264
|
-
(
|
265
|
-
f
|
266
|
-
for f in self._get_fields(reverse=False)
|
267
|
-
if is_not_an_m2m_field(f)
|
268
|
-
and is_not_a_generic_relation(f)
|
269
|
-
and is_not_a_generic_foreign_key(f)
|
270
|
-
),
|
271
|
-
)
|
272
|
-
|
273
|
-
@cached_property
|
274
|
-
def concrete_fields(self) -> ImmutableList:
|
275
|
-
"""
|
276
|
-
Return a list of all concrete fields on the model and its parents.
|
277
|
-
|
278
|
-
Private API intended only to be used by Plain itself; get_fields()
|
279
|
-
combined with filtering of field properties is the public API for
|
280
|
-
obtaining this field list.
|
281
|
-
"""
|
282
|
-
return make_immutable_fields_list(
|
283
|
-
"concrete_fields", (f for f in self.fields if f.concrete)
|
284
|
-
)
|
285
|
-
|
286
|
-
@cached_property
|
287
|
-
def local_concrete_fields(self) -> ImmutableList:
|
288
|
-
"""
|
289
|
-
Return a list of all concrete fields on the model.
|
290
|
-
|
291
|
-
Private API intended only to be used by Plain itself; get_fields()
|
292
|
-
combined with filtering of field properties is the public API for
|
293
|
-
obtaining this field list.
|
294
|
-
"""
|
295
|
-
return make_immutable_fields_list(
|
296
|
-
"local_concrete_fields", (f for f in self.local_fields if f.concrete)
|
297
|
-
)
|
298
|
-
|
299
|
-
@cached_property
|
300
|
-
def many_to_many(self) -> ImmutableList:
|
301
|
-
"""
|
302
|
-
Return a list of all many to many fields on the model and its parents.
|
303
|
-
|
304
|
-
Private API intended only to be used by Plain itself; get_fields()
|
305
|
-
combined with filtering of field properties is the public API for
|
306
|
-
obtaining this list.
|
307
|
-
"""
|
308
|
-
return make_immutable_fields_list(
|
309
|
-
"many_to_many",
|
310
|
-
(
|
311
|
-
f
|
312
|
-
for f in self._get_fields(reverse=False)
|
313
|
-
if f.is_relation and f.many_to_many
|
314
|
-
),
|
315
|
-
)
|
316
|
-
|
317
|
-
@cached_property
|
318
|
-
def related_objects(self) -> ImmutableList:
|
319
|
-
"""
|
320
|
-
Return all related objects pointing to the current model. The related
|
321
|
-
objects can come from a one-to-one, one-to-many, or many-to-many field
|
322
|
-
relation type.
|
323
|
-
|
324
|
-
Private API intended only to be used by Plain itself; get_fields()
|
325
|
-
combined with filtering of field properties is the public API for
|
326
|
-
obtaining this field list.
|
327
|
-
"""
|
328
|
-
all_related_fields = self._get_fields(
|
329
|
-
forward=False, reverse=True, include_hidden=True
|
330
|
-
)
|
331
|
-
return make_immutable_fields_list(
|
332
|
-
"related_objects",
|
333
|
-
(
|
334
|
-
obj
|
335
|
-
for obj in all_related_fields
|
336
|
-
if not obj.hidden or obj.field.many_to_many
|
337
|
-
),
|
338
|
-
)
|
339
|
-
|
340
|
-
@cached_property
|
341
|
-
def _forward_fields_map(self) -> dict[str, Any]:
|
342
|
-
res = {}
|
343
|
-
fields = self._get_fields(reverse=False)
|
344
|
-
for field in fields:
|
345
|
-
res[field.name] = field
|
346
|
-
# Due to the way Plain's internals work, get_field() should also
|
347
|
-
# be able to fetch a field by attname. In the case of a concrete
|
348
|
-
# field with relation, includes the *_id name too
|
349
|
-
try:
|
350
|
-
res[field.attname] = field
|
351
|
-
except AttributeError:
|
352
|
-
pass
|
353
|
-
return res
|
354
|
-
|
355
|
-
@cached_property
|
356
|
-
def fields_map(self) -> dict[str, Any]:
|
357
|
-
res = {}
|
358
|
-
fields = self._get_fields(forward=False, include_hidden=True)
|
359
|
-
for field in fields:
|
360
|
-
res[field.name] = field
|
361
|
-
# Due to the way Plain's internals work, get_field() should also
|
362
|
-
# be able to fetch a field by attname. In the case of a concrete
|
363
|
-
# field with relation, includes the *_id name too
|
364
|
-
try:
|
365
|
-
res[field.attname] = field
|
366
|
-
except AttributeError:
|
367
|
-
pass
|
368
|
-
return res
|
369
|
-
|
370
|
-
def get_field(self, field_name: str) -> Any:
|
371
|
-
"""
|
372
|
-
Return a field instance given the name of a forward or reverse field.
|
373
|
-
"""
|
374
|
-
try:
|
375
|
-
# In order to avoid premature loading of the relation tree
|
376
|
-
# (expensive) we prefer checking if the field is a forward field.
|
377
|
-
return self._forward_fields_map[field_name]
|
378
|
-
except KeyError:
|
379
|
-
# If the app registry is not ready, reverse fields are
|
380
|
-
# unavailable, therefore we throw a FieldDoesNotExist exception.
|
381
|
-
if not self.models_registry.ready:
|
382
|
-
raise FieldDoesNotExist(
|
383
|
-
f"{self.object_name} has no field named '{field_name}'. The app cache isn't ready yet, "
|
384
|
-
"so if this is an auto-created related field, it won't "
|
385
|
-
"be available yet."
|
386
|
-
)
|
387
|
-
|
388
|
-
try:
|
389
|
-
# Retrieve field instance by name from cached or just-computed
|
390
|
-
# field map.
|
391
|
-
return self.fields_map[field_name]
|
392
|
-
except KeyError:
|
393
|
-
raise FieldDoesNotExist(
|
394
|
-
f"{self.object_name} has no field named '{field_name}'"
|
395
|
-
)
|
396
|
-
|
397
|
-
def _populate_directed_relation_graph(self) -> Any:
|
398
|
-
"""
|
399
|
-
This method is used by each model to find its reverse objects. As this
|
400
|
-
method is very expensive and is accessed frequently (it looks up every
|
401
|
-
field in a model, in every app), it is computed on first access and then
|
402
|
-
is set as a property on every model.
|
403
|
-
"""
|
404
|
-
related_objects_graph: defaultdict[str, list[Any]] = defaultdict(list)
|
405
|
-
|
406
|
-
all_models = self.models_registry.get_models()
|
407
|
-
for model in all_models:
|
408
|
-
opts = model._meta
|
409
|
-
|
410
|
-
fields_with_relations = (
|
411
|
-
f
|
412
|
-
for f in opts._get_fields(reverse=False)
|
413
|
-
if f.is_relation and f.related_model is not None
|
414
|
-
)
|
415
|
-
for f in fields_with_relations:
|
416
|
-
if not isinstance(f.remote_field.model, str):
|
417
|
-
remote_label = f.remote_field.model._meta.label
|
418
|
-
related_objects_graph[remote_label].append(f)
|
419
|
-
|
420
|
-
for model in all_models:
|
421
|
-
# Set the relation_tree using the internal __dict__. In this way
|
422
|
-
# we avoid calling the cached property. In attribute lookup,
|
423
|
-
# __dict__ takes precedence over a data descriptor (such as
|
424
|
-
# @cached_property). This means that the _meta._relation_tree is
|
425
|
-
# only called if related_objects is not in __dict__.
|
426
|
-
related_objects = related_objects_graph[model._meta.label]
|
427
|
-
model._meta.__dict__["_relation_tree"] = related_objects
|
428
|
-
# It seems it is possible that self is not in all_models, so guard
|
429
|
-
# against that with default for get().
|
430
|
-
return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE)
|
431
|
-
|
432
|
-
@cached_property
|
433
|
-
def _relation_tree(self) -> Any:
|
434
|
-
return self._populate_directed_relation_graph()
|
435
|
-
|
436
|
-
def _expire_cache(self, forward: bool = True, reverse: bool = True) -> None:
|
437
|
-
# This method is usually called by packages.cache_clear(), when the
|
438
|
-
# registry is finalized, or when a new field is added.
|
439
|
-
if forward:
|
440
|
-
for cache_key in self.FORWARD_PROPERTIES:
|
441
|
-
if cache_key in self.__dict__:
|
442
|
-
delattr(self, cache_key)
|
443
|
-
if reverse:
|
444
|
-
for cache_key in self.REVERSE_PROPERTIES:
|
445
|
-
if cache_key in self.__dict__:
|
446
|
-
delattr(self, cache_key)
|
447
|
-
self._get_fields_cache = {}
|
448
|
-
|
449
|
-
def get_fields(self, include_hidden: bool = False) -> ImmutableList:
|
450
|
-
"""
|
451
|
-
Return a list of fields associated to the model. By default, include
|
452
|
-
forward and reverse fields, fields derived from inheritance, but not
|
453
|
-
hidden fields. The returned fields can be changed using the parameters:
|
454
|
-
|
455
|
-
- include_hidden: include fields that have a related_name that
|
456
|
-
starts with a "+"
|
457
|
-
"""
|
458
|
-
return self._get_fields(include_hidden=include_hidden)
|
459
|
-
|
460
|
-
def _get_fields(
|
461
|
-
self,
|
462
|
-
forward: bool = True,
|
463
|
-
reverse: bool = True,
|
464
|
-
include_hidden: bool = False,
|
465
|
-
seen_models: set[type[Any]] | None = None,
|
466
|
-
) -> ImmutableList:
|
467
|
-
"""
|
468
|
-
Internal helper function to return fields of the model.
|
469
|
-
* If forward=True, then fields defined on this model are returned.
|
470
|
-
* If reverse=True, then relations pointing to this model are returned.
|
471
|
-
* If include_hidden=True, then fields with is_hidden=True are returned.
|
472
|
-
"""
|
473
|
-
|
474
|
-
# This helper function is used to allow recursion in ``get_fields()``
|
475
|
-
# implementation and to provide a fast way for Plain's internals to
|
476
|
-
# access specific subsets of fields.
|
477
|
-
|
478
|
-
# We must keep track of which models we have already seen. Otherwise we
|
479
|
-
# could include the same field multiple times from different models.
|
480
|
-
topmost_call = seen_models is None
|
481
|
-
if seen_models is None:
|
482
|
-
seen_models = set()
|
483
|
-
seen_models.add(self.model)
|
484
|
-
|
485
|
-
# Creates a cache key composed of all arguments
|
486
|
-
cache_key = (forward, reverse, include_hidden, topmost_call)
|
487
|
-
|
488
|
-
try:
|
489
|
-
# In order to avoid list manipulation. Always return a shallow copy
|
490
|
-
# of the results.
|
491
|
-
return self._get_fields_cache[cache_key]
|
492
|
-
except KeyError:
|
493
|
-
pass
|
494
|
-
|
495
|
-
fields = []
|
496
|
-
|
497
|
-
if reverse:
|
498
|
-
# Tree is computed once and cached until the app cache is expired.
|
499
|
-
# It is composed of a list of fields pointing to the current model
|
500
|
-
# from other models.
|
501
|
-
all_fields = self._relation_tree
|
502
|
-
for field in all_fields:
|
503
|
-
# If hidden fields should be included or the relation is not
|
504
|
-
# intentionally hidden, add to the fields dict.
|
505
|
-
if include_hidden or not field.remote_field.hidden:
|
506
|
-
fields.append(field.remote_field)
|
507
|
-
|
508
|
-
if forward:
|
509
|
-
fields += self.local_fields
|
510
|
-
fields += self.local_many_to_many
|
511
|
-
|
512
|
-
# In order to avoid list manipulation. Always
|
513
|
-
# return a shallow copy of the results
|
514
|
-
fields = make_immutable_fields_list("get_fields()", fields)
|
515
|
-
|
516
|
-
# Store result into cache for later access
|
517
|
-
self._get_fields_cache[cache_key] = fields
|
518
|
-
return fields
|
519
|
-
|
520
|
-
@cached_property
|
521
|
-
def total_unique_constraints(self) -> list[UniqueConstraint]:
|
198
|
+
def total_unique_constraints(self) -> list[Any]:
|
522
199
|
"""
|
523
200
|
Return a list of total unique constraints. Useful for determining set
|
524
201
|
of fields guaranteed to be unique for all rows.
|
525
202
|
"""
|
203
|
+
from plain.models.constraints import UniqueConstraint
|
204
|
+
|
526
205
|
return [
|
527
206
|
constraint
|
528
207
|
for constraint in self.constraints
|
@@ -533,37 +212,22 @@ class Options:
|
|
533
212
|
)
|
534
213
|
]
|
535
214
|
|
536
|
-
|
537
|
-
def _property_names(self) -> frozenset[str]:
|
538
|
-
"""Return a set of the names of the properties defined on the model."""
|
539
|
-
names = []
|
540
|
-
for name in dir(self.model):
|
541
|
-
attr = inspect.getattr_static(self.model, name)
|
542
|
-
if isinstance(attr, property):
|
543
|
-
names.append(name)
|
544
|
-
return frozenset(names)
|
545
|
-
|
546
|
-
@cached_property
|
547
|
-
def _non_pk_concrete_field_names(self) -> frozenset[str]:
|
215
|
+
def can_migrate(self, connection: Any) -> bool:
|
548
216
|
"""
|
549
|
-
Return
|
550
|
-
|
551
|
-
names = []
|
552
|
-
for field in self.concrete_fields:
|
553
|
-
if not field.primary_key:
|
554
|
-
names.append(field.name)
|
555
|
-
if field.name != field.attname:
|
556
|
-
names.append(field.attname)
|
557
|
-
return frozenset(names)
|
558
|
-
|
559
|
-
@cached_property
|
560
|
-
def db_returning_fields(self) -> list[Field]:
|
561
|
-
"""
|
562
|
-
Private API intended only to be used by Plain itself.
|
563
|
-
Fields to be returned after a database insert.
|
217
|
+
Return True if the model can/should be migrated on the given
|
218
|
+
`connection` object.
|
564
219
|
"""
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
220
|
+
if self.required_db_vendor:
|
221
|
+
return self.required_db_vendor == connection.vendor
|
222
|
+
if self.required_db_features:
|
223
|
+
return all(
|
224
|
+
getattr(connection.features, feat, False)
|
225
|
+
for feat in self.required_db_features
|
226
|
+
)
|
227
|
+
return True
|
228
|
+
|
229
|
+
def __repr__(self) -> str:
|
230
|
+
return f"<Options for {self.model.__name__}>"
|
231
|
+
|
232
|
+
def __str__(self) -> str:
|
233
|
+
return f"{self.package_label}.{self.model.__name__.lower()}"
|