plain.models 0.42.0__tar.gz → 0.43.0__tar.gz
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-0.42.0 → plain_models-0.43.0}/PKG-INFO +1 -1
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/CHANGELOG.md +15 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/base.py +3 -2
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/related.py +6 -28
- plain_models-0.43.0/plain/models/fields/related_descriptors.py +393 -0
- plain_models-0.43.0/plain/models/fields/related_managers.py +629 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/reverse_related.py +4 -7
- {plain_models-0.42.0 → plain_models-0.43.0}/pyproject.toml +1 -1
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/models.py +3 -1
- plain_models-0.42.0/plain/models/fields/related_descriptors.py +0 -947
- {plain_models-0.42.0 → plain_models-0.43.0}/.gitignore +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/LICENSE +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/README.md +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/AGENTS.md +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/README.md +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/aggregates.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/creation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/schema.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/compiler.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/operations.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/utils.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/cli.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/clients.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/core.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/cli.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/config.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/connections.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/constants.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/constraints.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/database_url.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/db.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/default_settings.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/deletion.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/entrypoints.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/enums.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/exceptions.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/expressions.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/json.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/related_lookups.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/forms.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/math.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/text.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/window.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/indexes.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/lookups.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/autodetector.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/executor.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/loader.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/migration.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/models.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/special.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/recorder.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/serializer.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/state.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/options.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/otel.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/preflight.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/query.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/query_utils.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/registry.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/compiler.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/constants.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/datastructures.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/query.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/subqueries.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/where.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/pytest.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/utils.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/transaction.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/utils.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/settings.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/urls.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_database_url.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_delete_behaviors.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_exceptions.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_manager_assignment.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_models.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_related_descriptors.py +0 -0
- {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_related_manager_api.py +0 -0
@@ -1,5 +1,20 @@
|
|
1
1
|
# plain-models changelog
|
2
2
|
|
3
|
+
## [0.43.0](https://github.com/dropseed/plain/releases/plain-models@0.43.0) (2025-09-12)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- The `related_name` parameter is now required for ForeignKey and ManyToManyField relationships if you want a reverse accessor. The `"+"` suffix to disable reverse relations has been removed, and automatic `_set` suffixes are no longer generated ([89fa03979f](https://github.com/dropseed/plain/commit/89fa03979f))
|
8
|
+
- Refactored related descriptors and managers for better internal organization and type safety ([9f0b03957a](https://github.com/dropseed/plain/commit/9f0b03957a))
|
9
|
+
- Added docstrings and return type annotations to model `query` property and related manager methods for improved developer experience ([544d85b60b](https://github.com/dropseed/plain/commit/544d85b60b))
|
10
|
+
|
11
|
+
### Upgrade instructions
|
12
|
+
|
13
|
+
- Remove any `related_name="+"` usage - if you don't want a reverse accessor, simply omit the `related_name` parameter entirely
|
14
|
+
- Update any code that relied on automatic `_set` suffixes - these are no longer generated, so you must use explicit `related_name` values
|
15
|
+
- Add explicit `related_name` arguments to all ForeignKey and ManyToManyField definitions where you want reverse access (e.g., `models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")`)
|
16
|
+
- Consider removing `related_name` arguments that are not used in practice
|
17
|
+
|
3
18
|
## [0.42.0](https://github.com/dropseed/plain/releases/plain-models@0.42.0) (2025-09-12)
|
4
19
|
|
5
20
|
### What's changed
|
@@ -25,7 +25,7 @@ from plain.models.expressions import RawSQL, Value
|
|
25
25
|
from plain.models.fields import NOT_PROVIDED
|
26
26
|
from plain.models.fields.reverse_related import ForeignObjectRel
|
27
27
|
from plain.models.options import Options
|
28
|
-
from plain.models.query import F, Q
|
28
|
+
from plain.models.query import F, Q, QuerySet
|
29
29
|
from plain.packages import packages_registry
|
30
30
|
from plain.utils.encoding import force_str
|
31
31
|
from plain.utils.hashable import make_hashable
|
@@ -168,7 +168,8 @@ class ModelBase(type):
|
|
168
168
|
index.set_name_with_model(cls)
|
169
169
|
|
170
170
|
@property
|
171
|
-
def query(cls):
|
171
|
+
def query(cls) -> QuerySet:
|
172
|
+
"""Create a new QuerySet for this model."""
|
172
173
|
return cls._meta.queryset
|
173
174
|
|
174
175
|
|
@@ -12,8 +12,9 @@ from . import Field
|
|
12
12
|
from .mixins import FieldCacheMixin
|
13
13
|
from .related_descriptors import (
|
14
14
|
ForeignKeyDeferredAttribute,
|
15
|
+
ForwardManyToManyDescriptor,
|
15
16
|
ForwardManyToOneDescriptor,
|
16
|
-
|
17
|
+
ReverseManyToManyDescriptor,
|
17
18
|
ReverseManyToOneDescriptor,
|
18
19
|
)
|
19
20
|
from .related_lookups import (
|
@@ -123,14 +124,11 @@ class RelatedField(FieldCacheMixin, Field):
|
|
123
124
|
is_valid_id = (
|
124
125
|
not keyword.iskeyword(related_name) and related_name.isidentifier()
|
125
126
|
)
|
126
|
-
if not
|
127
|
+
if not is_valid_id:
|
127
128
|
return [
|
128
129
|
preflight.Error(
|
129
130
|
f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model._meta.object_name}.{self.name}",
|
130
|
-
hint=
|
131
|
-
"Related name must be a valid Python identifier or end with a "
|
132
|
-
"'+'"
|
133
|
-
),
|
131
|
+
hint="Related name must be a valid Python identifier.",
|
134
132
|
obj=self,
|
135
133
|
id="fields.E306",
|
136
134
|
)
|
@@ -1236,26 +1234,6 @@ class ManyToManyField(RelatedField):
|
|
1236
1234
|
return getattr(self, cache_attr)
|
1237
1235
|
|
1238
1236
|
def contribute_to_class(self, cls, name, **kwargs):
|
1239
|
-
# To support multiple relations to self, it's useful to have a non-None
|
1240
|
-
# related name on symmetrical relations for internal reasons. The
|
1241
|
-
# concept doesn't make a lot of sense externally ("you want me to
|
1242
|
-
# specify *what* on my non-reversible relation?!"), so we set it up
|
1243
|
-
# automatically. The funky name reduces the chance of an accidental
|
1244
|
-
# clash.
|
1245
|
-
if self.remote_field.symmetrical and (
|
1246
|
-
self.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT
|
1247
|
-
or self.remote_field.model == cls._meta.object_name
|
1248
|
-
):
|
1249
|
-
self.remote_field.related_name = f"{name}_rel_+"
|
1250
|
-
elif self.remote_field.is_hidden():
|
1251
|
-
# If the backwards relation is disabled, replace the original
|
1252
|
-
# related_name with one generated from the m2m field name. Plain
|
1253
|
-
# still uses backwards relations internally and we need to avoid
|
1254
|
-
# clashes between multiple m2m fields with related_name == '+'.
|
1255
|
-
self.remote_field.related_name = (
|
1256
|
-
f"_{cls._meta.package_label}_{cls.__name__.lower()}_{name}_+"
|
1257
|
-
)
|
1258
|
-
|
1259
1237
|
super().contribute_to_class(cls, name, **kwargs)
|
1260
1238
|
|
1261
1239
|
def resolve_through_model(_, model, field):
|
@@ -1266,7 +1244,7 @@ class ManyToManyField(RelatedField):
|
|
1266
1244
|
)
|
1267
1245
|
|
1268
1246
|
# Add the descriptor for the m2m relation.
|
1269
|
-
setattr(cls, self.name,
|
1247
|
+
setattr(cls, self.name, ForwardManyToManyDescriptor(self.remote_field))
|
1270
1248
|
|
1271
1249
|
# Set up the accessor for the m2m table name for the relation.
|
1272
1250
|
self.m2m_db_table = self._get_m2m_db_table
|
@@ -1278,7 +1256,7 @@ class ManyToManyField(RelatedField):
|
|
1278
1256
|
setattr(
|
1279
1257
|
cls,
|
1280
1258
|
related.get_accessor_name(),
|
1281
|
-
|
1259
|
+
ReverseManyToManyDescriptor(self.remote_field),
|
1282
1260
|
)
|
1283
1261
|
|
1284
1262
|
# Set up the accessors for the column names on the m2m table.
|
@@ -0,0 +1,393 @@
|
|
1
|
+
"""
|
2
|
+
Accessors for related objects.
|
3
|
+
|
4
|
+
When a field defines a relation between two models, each model class provides
|
5
|
+
an attribute to access related instances of the other model class (unless the
|
6
|
+
reverse accessor has been disabled with related_name='+').
|
7
|
+
|
8
|
+
Accessors are implemented as descriptors in order to customize access and
|
9
|
+
assignment. This module defines the descriptor classes.
|
10
|
+
|
11
|
+
Forward accessors follow foreign keys. Reverse accessors trace them back. For
|
12
|
+
example, with the following models::
|
13
|
+
|
14
|
+
class Parent(Model):
|
15
|
+
pass
|
16
|
+
|
17
|
+
class Child(Model):
|
18
|
+
parent = ForeignKey(Parent, related_name='children')
|
19
|
+
|
20
|
+
``child.parent`` is a forward many-to-one relation. ``parent.children`` is a
|
21
|
+
reverse many-to-one relation.
|
22
|
+
|
23
|
+
1. Related instance on the forward side of a many-to-one relation:
|
24
|
+
``ForwardManyToOneDescriptor``.
|
25
|
+
|
26
|
+
Uniqueness of foreign key values is irrelevant to accessing the related
|
27
|
+
instance, making the many-to-one and one-to-one cases identical as far as
|
28
|
+
the descriptor is concerned. The constraint is checked upstream (unicity
|
29
|
+
validation in forms) or downstream (unique indexes in the database).
|
30
|
+
|
31
|
+
2. Related objects manager for related instances on the reverse side of a
|
32
|
+
many-to-one relation: ``ReverseManyToOneDescriptor``.
|
33
|
+
|
34
|
+
Unlike the previous two classes, this one provides access to a collection
|
35
|
+
of objects. It returns a manager rather than an instance.
|
36
|
+
|
37
|
+
3. Related objects manager for related instances on the forward or reverse
|
38
|
+
sides of a many-to-many relation: ``ManyToManyDescriptor``.
|
39
|
+
|
40
|
+
Many-to-many relations are symmetrical. The syntax of Plain models
|
41
|
+
requires declaring them on one side but that's an implementation detail.
|
42
|
+
They could be declared on the other side without any change in behavior.
|
43
|
+
Therefore the forward and reverse descriptors can be the same.
|
44
|
+
|
45
|
+
If you're looking for ``ForwardManyToManyDescriptor`` or
|
46
|
+
``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
|
47
|
+
"""
|
48
|
+
|
49
|
+
from functools import cached_property
|
50
|
+
|
51
|
+
from plain.models.query import QuerySet
|
52
|
+
from plain.models.query_utils import DeferredAttribute
|
53
|
+
from plain.utils.functional import LazyObject
|
54
|
+
|
55
|
+
from .related_managers import (
|
56
|
+
ForwardManyToManyManager,
|
57
|
+
ReverseManyToManyManager,
|
58
|
+
ReverseManyToOneManager,
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
class ForeignKeyDeferredAttribute(DeferredAttribute):
|
63
|
+
def __set__(self, instance, value):
|
64
|
+
if instance.__dict__.get(self.field.attname) != value and self.field.is_cached(
|
65
|
+
instance
|
66
|
+
):
|
67
|
+
self.field.delete_cached_value(instance)
|
68
|
+
instance.__dict__[self.field.attname] = value
|
69
|
+
|
70
|
+
|
71
|
+
class ForwardManyToOneDescriptor:
|
72
|
+
"""
|
73
|
+
Accessor to the related object on the forward side of a many-to-one relation.
|
74
|
+
|
75
|
+
In the example::
|
76
|
+
|
77
|
+
class Child(Model):
|
78
|
+
parent = ForeignKey(Parent, related_name='children')
|
79
|
+
|
80
|
+
``Child.parent`` is a ``ForwardManyToOneDescriptor`` instance.
|
81
|
+
"""
|
82
|
+
|
83
|
+
def __init__(self, field_with_rel):
|
84
|
+
self.field = field_with_rel
|
85
|
+
|
86
|
+
@cached_property
|
87
|
+
def RelatedObjectDoesNotExist(self):
|
88
|
+
# The exception can't be created at initialization time since the
|
89
|
+
# related model might not be resolved yet; `self.field.model` might
|
90
|
+
# still be a string model reference.
|
91
|
+
return type(
|
92
|
+
"RelatedObjectDoesNotExist",
|
93
|
+
(self.field.remote_field.model.DoesNotExist, AttributeError),
|
94
|
+
{
|
95
|
+
"__module__": self.field.model.__module__,
|
96
|
+
"__qualname__": f"{self.field.model.__qualname__}.{self.field.name}.RelatedObjectDoesNotExist",
|
97
|
+
},
|
98
|
+
)
|
99
|
+
|
100
|
+
def is_cached(self, instance):
|
101
|
+
return self.field.is_cached(instance)
|
102
|
+
|
103
|
+
def get_queryset(self) -> QuerySet:
|
104
|
+
qs = self.field.remote_field.model._meta.base_queryset
|
105
|
+
return qs.all()
|
106
|
+
|
107
|
+
def get_prefetch_queryset(self, instances, queryset=None):
|
108
|
+
if queryset is None:
|
109
|
+
queryset = self.get_queryset()
|
110
|
+
|
111
|
+
rel_obj_attr = self.field.get_foreign_related_value
|
112
|
+
instance_attr = self.field.get_local_related_value
|
113
|
+
instances_dict = {instance_attr(inst): inst for inst in instances}
|
114
|
+
related_field = self.field.foreign_related_fields[0]
|
115
|
+
remote_field = self.field.remote_field
|
116
|
+
|
117
|
+
# FIXME: This will need to be revisited when we introduce support for
|
118
|
+
# composite fields. In the meantime we take this practical approach to
|
119
|
+
# solve a regression on 1.6 when the reverse manager in hidden
|
120
|
+
# (related_name ends with a '+'). Refs #21410.
|
121
|
+
# The check for len(...) == 1 is a special case that allows the query
|
122
|
+
# to be join-less and smaller. Refs #21760.
|
123
|
+
if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
|
124
|
+
query = {
|
125
|
+
f"{related_field.name}__in": {
|
126
|
+
instance_attr(inst)[0] for inst in instances
|
127
|
+
}
|
128
|
+
}
|
129
|
+
else:
|
130
|
+
query = {f"{self.field.related_query_name()}__in": instances}
|
131
|
+
queryset = queryset.filter(**query)
|
132
|
+
|
133
|
+
# Since we're going to assign directly in the cache,
|
134
|
+
# we must manage the reverse relation cache manually.
|
135
|
+
if not remote_field.multiple:
|
136
|
+
for rel_obj in queryset:
|
137
|
+
instance = instances_dict[rel_obj_attr(rel_obj)]
|
138
|
+
remote_field.set_cached_value(rel_obj, instance)
|
139
|
+
return (
|
140
|
+
queryset,
|
141
|
+
rel_obj_attr,
|
142
|
+
instance_attr,
|
143
|
+
True,
|
144
|
+
self.field.get_cache_name(),
|
145
|
+
False,
|
146
|
+
)
|
147
|
+
|
148
|
+
def get_object(self, instance):
|
149
|
+
qs = self.get_queryset()
|
150
|
+
# Assuming the database enforces foreign keys, this won't fail.
|
151
|
+
return qs.get(self.field.get_reverse_related_filter(instance))
|
152
|
+
|
153
|
+
def __get__(self, instance, cls=None):
|
154
|
+
"""
|
155
|
+
Get the related instance through the forward relation.
|
156
|
+
|
157
|
+
With the example above, when getting ``child.parent``:
|
158
|
+
|
159
|
+
- ``self`` is the descriptor managing the ``parent`` attribute
|
160
|
+
- ``instance`` is the ``child`` instance
|
161
|
+
- ``cls`` is the ``Child`` class (we don't need it)
|
162
|
+
"""
|
163
|
+
if instance is None:
|
164
|
+
return self
|
165
|
+
|
166
|
+
# The related instance is loaded from the database and then cached
|
167
|
+
# by the field on the model instance state. It can also be pre-cached
|
168
|
+
# by the reverse accessor.
|
169
|
+
try:
|
170
|
+
rel_obj = self.field.get_cached_value(instance)
|
171
|
+
except KeyError:
|
172
|
+
has_value = None not in self.field.get_local_related_value(instance)
|
173
|
+
rel_obj = None
|
174
|
+
|
175
|
+
if rel_obj is None and has_value:
|
176
|
+
rel_obj = self.get_object(instance)
|
177
|
+
remote_field = self.field.remote_field
|
178
|
+
# If this is a one-to-one relation, set the reverse accessor
|
179
|
+
# cache on the related object to the current instance to avoid
|
180
|
+
# an extra SQL query if it's accessed later on.
|
181
|
+
if not remote_field.multiple:
|
182
|
+
remote_field.set_cached_value(rel_obj, instance)
|
183
|
+
self.field.set_cached_value(instance, rel_obj)
|
184
|
+
|
185
|
+
if rel_obj is None and not self.field.allow_null:
|
186
|
+
raise self.RelatedObjectDoesNotExist(
|
187
|
+
f"{self.field.model.__name__} has no {self.field.name}."
|
188
|
+
)
|
189
|
+
else:
|
190
|
+
return rel_obj
|
191
|
+
|
192
|
+
def __set__(self, instance, value):
|
193
|
+
"""
|
194
|
+
Set the related instance through the forward relation.
|
195
|
+
|
196
|
+
With the example above, when setting ``child.parent = parent``:
|
197
|
+
|
198
|
+
- ``self`` is the descriptor managing the ``parent`` attribute
|
199
|
+
- ``instance`` is the ``child`` instance
|
200
|
+
- ``value`` is the ``parent`` instance on the right of the equal sign
|
201
|
+
"""
|
202
|
+
# If value is a LazyObject (like SimpleLazyObject used for request.user),
|
203
|
+
# force its evaluation. For ForeignKey fields, the value should only be
|
204
|
+
# None or a model instance, never a boolean or other type.
|
205
|
+
if isinstance(value, LazyObject):
|
206
|
+
# This forces evaluation: if it's None, value becomes None;
|
207
|
+
# if it's a User instance, value becomes that instance.
|
208
|
+
value = value if value else None
|
209
|
+
|
210
|
+
# An object must be an instance of the related class.
|
211
|
+
if value is not None and not isinstance(
|
212
|
+
value, self.field.remote_field.model._meta.concrete_model
|
213
|
+
):
|
214
|
+
raise ValueError(
|
215
|
+
f'Cannot assign "{value!r}": "{instance._meta.object_name}.{self.field.name}" must be a "{self.field.remote_field.model._meta.object_name}" instance.'
|
216
|
+
)
|
217
|
+
remote_field = self.field.remote_field
|
218
|
+
# If we're setting the value of a OneToOneField to None, we need to clear
|
219
|
+
# out the cache on any old related object. Otherwise, deleting the
|
220
|
+
# previously-related object will also cause this object to be deleted,
|
221
|
+
# which is wrong.
|
222
|
+
if value is None:
|
223
|
+
# Look up the previously-related object, which may still be available
|
224
|
+
# since we've not yet cleared out the related field.
|
225
|
+
# Use the cache directly, instead of the accessor; if we haven't
|
226
|
+
# populated the cache, then we don't care - we're only accessing
|
227
|
+
# the object to invalidate the accessor cache, so there's no
|
228
|
+
# need to populate the cache just to expire it again.
|
229
|
+
related = self.field.get_cached_value(instance, default=None)
|
230
|
+
|
231
|
+
# If we've got an old related object, we need to clear out its
|
232
|
+
# cache. This cache also might not exist if the related object
|
233
|
+
# hasn't been accessed yet.
|
234
|
+
if related is not None:
|
235
|
+
remote_field.set_cached_value(related, None)
|
236
|
+
|
237
|
+
for lh_field, rh_field in self.field.related_fields:
|
238
|
+
setattr(instance, lh_field.attname, None)
|
239
|
+
|
240
|
+
# Set the values of the related field.
|
241
|
+
else:
|
242
|
+
for lh_field, rh_field in self.field.related_fields:
|
243
|
+
setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
|
244
|
+
|
245
|
+
# Set the related instance cache used by __get__ to avoid an SQL query
|
246
|
+
# when accessing the attribute we just set.
|
247
|
+
self.field.set_cached_value(instance, value)
|
248
|
+
|
249
|
+
# If this is a one-to-one relation, set the reverse accessor cache on
|
250
|
+
# the related object to the current instance to avoid an extra SQL
|
251
|
+
# query if it's accessed later on.
|
252
|
+
if value is not None and not remote_field.multiple:
|
253
|
+
remote_field.set_cached_value(value, instance)
|
254
|
+
|
255
|
+
def __reduce__(self):
|
256
|
+
"""
|
257
|
+
Pickling should return the instance attached by self.field on the
|
258
|
+
model, not a new copy of that descriptor. Use getattr() to retrieve
|
259
|
+
the instance directly from the model.
|
260
|
+
"""
|
261
|
+
return getattr, (self.field.model, self.field.name)
|
262
|
+
|
263
|
+
|
264
|
+
class RelationDescriptorBase:
|
265
|
+
"""
|
266
|
+
Base class for relation descriptors that don't allow direct assignment.
|
267
|
+
|
268
|
+
This is used for descriptors that manage collections of related objects
|
269
|
+
(reverse FK and M2M relations). Forward FK relations don't inherit from
|
270
|
+
this because they allow direct assignment.
|
271
|
+
"""
|
272
|
+
|
273
|
+
def __init__(self, rel):
|
274
|
+
self.rel = rel
|
275
|
+
self.field = rel.field
|
276
|
+
|
277
|
+
def __get__(self, instance, cls=None):
|
278
|
+
"""
|
279
|
+
Get the related manager when the descriptor is accessed.
|
280
|
+
|
281
|
+
Subclasses must implement get_related_manager().
|
282
|
+
"""
|
283
|
+
if instance is None:
|
284
|
+
return self
|
285
|
+
return self.get_related_manager(instance)
|
286
|
+
|
287
|
+
def get_related_manager(self, instance):
|
288
|
+
"""Return the appropriate manager for this relation."""
|
289
|
+
raise NotImplementedError(
|
290
|
+
f"{self.__class__.__name__} must implement get_related_manager()"
|
291
|
+
)
|
292
|
+
|
293
|
+
def _get_set_deprecation_msg_params(self):
|
294
|
+
"""Return parameters for the error message when direct assignment is attempted."""
|
295
|
+
raise NotImplementedError(
|
296
|
+
f"{self.__class__.__name__} must implement _get_set_deprecation_msg_params()"
|
297
|
+
)
|
298
|
+
|
299
|
+
def __set__(self, instance, value):
|
300
|
+
"""Prevent direct assignment to the relation."""
|
301
|
+
raise TypeError(
|
302
|
+
"Direct assignment to the {} is prohibited. Use {}.set() instead.".format(
|
303
|
+
*self._get_set_deprecation_msg_params()
|
304
|
+
),
|
305
|
+
)
|
306
|
+
|
307
|
+
|
308
|
+
class ReverseManyToOneDescriptor(RelationDescriptorBase):
|
309
|
+
"""
|
310
|
+
Accessor to the related objects manager on the reverse side of a
|
311
|
+
many-to-one relation.
|
312
|
+
|
313
|
+
In the example::
|
314
|
+
|
315
|
+
class Child(Model):
|
316
|
+
parent = ForeignKey(Parent, related_name='children')
|
317
|
+
|
318
|
+
``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
|
319
|
+
|
320
|
+
Most of the implementation is delegated to the ReverseManyToOneManager class.
|
321
|
+
"""
|
322
|
+
|
323
|
+
def get_related_manager(self, instance):
|
324
|
+
"""Return the ReverseManyToOneManager for this relation."""
|
325
|
+
return ReverseManyToOneManager(instance, self.rel)
|
326
|
+
|
327
|
+
def _get_set_deprecation_msg_params(self):
|
328
|
+
return (
|
329
|
+
"reverse side of a related set",
|
330
|
+
self.rel.get_accessor_name(),
|
331
|
+
)
|
332
|
+
|
333
|
+
|
334
|
+
class ForwardManyToManyDescriptor(RelationDescriptorBase):
|
335
|
+
"""
|
336
|
+
Accessor to the related objects manager on the forward side of a
|
337
|
+
many-to-many relation.
|
338
|
+
|
339
|
+
In the example::
|
340
|
+
|
341
|
+
class Pizza(Model):
|
342
|
+
toppings = ManyToManyField(Topping, related_name='pizzas')
|
343
|
+
|
344
|
+
``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
|
345
|
+
"""
|
346
|
+
|
347
|
+
@property
|
348
|
+
def through(self):
|
349
|
+
# through is provided so that you have easy access to the through
|
350
|
+
# model (Book.authors.through) for inlines, etc. This is done as
|
351
|
+
# a property to ensure that the fully resolved value is returned.
|
352
|
+
return self.rel.through
|
353
|
+
|
354
|
+
def get_related_manager(self, instance):
|
355
|
+
"""Return the ForwardManyToManyManager for this relation."""
|
356
|
+
return ForwardManyToManyManager(instance, self.rel)
|
357
|
+
|
358
|
+
def _get_set_deprecation_msg_params(self):
|
359
|
+
return (
|
360
|
+
"forward side of a many-to-many set",
|
361
|
+
self.field.name,
|
362
|
+
)
|
363
|
+
|
364
|
+
|
365
|
+
class ReverseManyToManyDescriptor(RelationDescriptorBase):
|
366
|
+
"""
|
367
|
+
Accessor to the related objects manager on the reverse side of a
|
368
|
+
many-to-many relation.
|
369
|
+
|
370
|
+
In the example::
|
371
|
+
|
372
|
+
class Pizza(Model):
|
373
|
+
toppings = ManyToManyField(Topping, related_name='pizzas')
|
374
|
+
|
375
|
+
``Topping.pizzas`` is a ``ReverseManyToManyDescriptor`` instance.
|
376
|
+
"""
|
377
|
+
|
378
|
+
@property
|
379
|
+
def through(self):
|
380
|
+
# through is provided so that you have easy access to the through
|
381
|
+
# model (Book.authors.through) for inlines, etc. This is done as
|
382
|
+
# a property to ensure that the fully resolved value is returned.
|
383
|
+
return self.rel.through
|
384
|
+
|
385
|
+
def get_related_manager(self, instance):
|
386
|
+
"""Return the ReverseManyToManyManager for this relation."""
|
387
|
+
return ReverseManyToManyManager(instance, self.rel)
|
388
|
+
|
389
|
+
def _get_set_deprecation_msg_params(self):
|
390
|
+
return (
|
391
|
+
"reverse side of a many-to-many set",
|
392
|
+
self.rel.get_accessor_name(),
|
393
|
+
)
|